Compare commits

...

21 Commits

Author SHA1 Message Date
134
7c1d985c09 perf: improve home and canvas initial load 2026-07-07 22:28:12 +08:00
134
67b526408a fix: use repair opacity mask for product lock scale composite 2026-07-07 22:21:15 +08:00
134
f46c0b192d fix: allow longer s3 uploads for canvas results 2026-07-07 21:49:10 +08:00
134
15fc9ebea9 fix: clip product lock repairs to soft mask 2026-07-07 21:30:57 +08:00
134
961ce30500 fix: avoid diff patching product lock mask edits 2026-07-07 21:06:09 +08:00
134
0a66dbc0bc fix: avoid openai for scale adjust mask edits 2026-07-07 20:36:47 +08:00
134
6b70865235 fix: reuse source url for product lock mask edits 2026-07-07 20:24:05 +08:00
134
c60a3bb226 fix: stabilize product lock scale adjust uploads 2026-07-07 20:08:40 +08:00
134
3828ec792d fix: honor fal scale adjust mask requests 2026-07-07 19:57:36 +08:00
134
40ce24feb8 fix: avoid broad mask in product lock scale adjust 2026-07-07 19:44:10 +08:00
134
a201ebb346 fix: use mask repair for product lock scale adjust 2026-07-06 19:16:23 +08:00
134
bf5d10f99f fix: keep guided product lock output product-free 2026-07-06 09:14:51 +08:00
134
56c1c96395 fix: guide product lock scale adjust with target draft 2026-07-06 08:54:20 +08:00
134
e4ec78f56e fix: broaden product lock background repair mask 2026-07-06 08:31:49 +08:00
134
5d4286f7c2 fix: keep person guard for product scale lock 2026-07-06 08:08:14 +08:00
134
376881d9f9 fix: upload source for product lock mask edit 2026-07-06 07:53:49 +08:00
134
abfc813e42 fix: use source image for product lock background edit 2026-07-06 07:42:11 +08:00
134
fde5661436 fix: enable product lock without person guard 2026-07-06 07:19:09 +08:00
134
bf8a03cf73 fix: preserve furniture cutouts in scale adjust 2026-07-06 07:05:17 +08:00
134
6b29a3acf7 fix: send fal gpt image edit mask field 2026-07-06 06:47:18 +08:00
134
82b277bdbd fix: repair product lock scale adjust background input 2026-07-06 06:39:32 +08:00
9 changed files with 375 additions and 78 deletions

View File

@ -126,12 +126,14 @@ type ScaleAdjustCropReference = {
type ScaleAdjustPatchReference = {
backgroundPlateFullUrl?: string | null;
compositeClipMaskUrl?: string | null;
productLockBackgroundRepairMaskUrl?: string | null;
productLockMethod?: string | null;
productLockEditMaskUrl?: string | null;
productLockFalEditMaskUrl?: string | null;
productLockOverlayUrl?: string | null;
productLockProtectMaskUrl?: string | null;
sourceCropUrl: string;
sourceFullUrl?: string | null;
structureLockedDraft?: boolean;
targetDraftFullUrl?: string | null;
targetDraftUrl: string;
@ -469,6 +471,22 @@ function resolveScaleAdjustPreferredModelId(params: {
const maskCapableGptImage2 = gptImage2Candidates
.filter(isScaleAdjustMaskCapableModel)
.filter((entry) => entry.model.healthStatus !== "auto_disabled");
const requestedMaskCapableGptImage2 = requestedModel
? maskCapableGptImage2.find(
(entry) =>
entry.model.id === requestedModel.model.id ||
entry.model.modelId === requestedModel.model.modelId
)
: null;
if (requestedMaskCapableGptImage2) {
return requestedMaskCapableGptImage2.model.id;
}
const nonOpenAiMaskCapableGptImage2 = maskCapableGptImage2.find(
(entry) => entry.provider.type !== "openai-image" && isScaleAdjustHealthyModel(entry)
);
if (nonOpenAiMaskCapableGptImage2) {
return nonOpenAiMaskCapableGptImage2.model.id;
}
if (maskCapableGptImage2[0]) {
return maskCapableGptImage2[0].model.id;
}
@ -2293,6 +2311,47 @@ async function buildScaleAdjustOpaqueEditMaskFromOpacityMask(params: {
.toBuffer();
}
async function buildScaleAdjustProductLockBackgroundRepairMaskBuffer(params: {
productSourceMaskBuffer: Buffer;
sourceHeight: number;
sourceWidth: number;
}) {
const {
productSourceMaskBuffer,
sourceHeight,
sourceWidth,
} = params;
const productAlpha = await sharp(productSourceMaskBuffer, { failOn: "none" })
.rotate()
.resize(sourceWidth, sourceHeight, { fit: "fill" })
.ensureAlpha()
.extractChannel(3)
.greyscale()
.raw()
.toBuffer();
const minSide = Math.max(1, Math.min(sourceWidth, sourceHeight));
const repairAlpha = await sharp(productAlpha, {
raw: { channels: 1, height: sourceHeight, width: sourceWidth },
})
.blur(Math.max(1.4, Math.min(10, Math.round(minSide * 0.0045))))
.raw()
.toBuffer();
const rgba = Buffer.alloc(sourceWidth * sourceHeight * 4);
for (let index = 0; index < sourceWidth * sourceHeight; index += 1) {
const offset = index * 4;
const value = Math.max(productAlpha[index] || 0, repairAlpha[index] || 0);
rgba[offset] = 255;
rgba[offset + 1] = 255;
rgba[offset + 2] = 255;
rgba[offset + 3] = value;
}
return sharp(rgba, {
raw: { channels: 4, height: sourceHeight, width: sourceWidth },
})
.png({ compressionLevel: 9 })
.toBuffer();
}
function getScaleAdjustAlphaBoundingBox(params: {
height: number;
rawRgba: Buffer;
@ -2523,6 +2582,7 @@ async function resizeScaleAdjustAlphaMask(params: {
async function buildScaleAdjustRmbgStructureLockedComposite(params: {
placement: ReturnType<typeof getScaleAdjustVisualGuidePlacement>;
preferPixelStructure?: boolean;
protectedPersonSide?: ScaleAdjustProtectedPersonSide | null;
raw: Buffer;
sourceBuffer: Buffer;
@ -2532,6 +2592,7 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
}): Promise<ScaleAdjustStructureLockedComposite | null> {
const {
placement,
preferPixelStructure = false,
protectedPersonSide = null,
raw,
sourceBuffer,
@ -2550,7 +2611,7 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
userId,
imageBase64: sourceCropBuffer.toString("base64"),
imageBuffer: sourceCropBuffer,
prompt: protectedPersonSide
prompt: preferPixelStructure || protectedPersonSide
? "比例调整家具/商品抠图:只保留框选区域内需要缩放的目标家具/商品主体和它本身的配件结构,明确排除非目标人物、手、脚、腿、衣服、书本、身体遮挡物和背景,输出透明背景 PNG"
: "比例调整主体抠图:只保留框选的目标人物/家具/商品主体,输出透明背景 PNG",
skipGallerySave: true,
@ -2605,16 +2666,9 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
}),
})
: null;
const protectedPersonRemovalMask =
protectedPersonMask ||
(protectedPersonSide && sameSizeCutout
? buildScaleAdjustProtectedPersonGeometryMask({
box: { width: cutoutWidth, height: cutoutHeight },
side: protectedPersonSide,
})
: null);
const protectedPersonRemovalMask = protectedPersonMask;
const sideProtectedCleanedAlpha =
protectedPersonSide && sameSizeCutout && sourceCropRaw
protectedPersonSide && sameSizeCutout && sourceCropRaw && !preferPixelStructure
? suppressScaleAdjustProtectedFurnitureDarkAlpha({
alphaMask: Buffer.from(cleanedAlpha),
bbox: { left: 0, top: 0, width: cutoutWidth, height: cutoutHeight },
@ -2837,14 +2891,7 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
.toBuffer();
const targetClipMaskPatch =
await buildScaleAdjustOverlayClipMaskPatch(structureOverlay);
const productLockProtectMask =
protectedPersonRemovalMask ||
(protectedPersonSide
? buildScaleAdjustProtectedPersonGeometryMask({
box: placement.sourceBox,
side: protectedPersonSide,
})
: null);
const productLockProtectMask = protectedPersonMask;
const productLockProtectMaskPatch = productLockProtectMask
? await buildScaleAdjustCompositeClipMaskPatch({
box: placement.sourceBox,
@ -2874,7 +2921,7 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
placedWidth,
protectedPersonSide,
protectedPersonGeometryMaskApplied:
Boolean(protectedPersonRemovalMask) && !protectedPersonMask,
false,
protectedPersonMaskApplied: Boolean(protectedPersonMask),
sameSizeCutout,
alphaBandingStats,
@ -3128,7 +3175,7 @@ async function buildScaleAdjustStructureLockedComposite(params: {
} = params;
const allowRmbgStructureLock =
process.env.CANVAS_SCALE_ADJUST_ENABLE_RMBG_PRODUCT_LOCK !== "0" &&
((preferPixelStructure && Boolean(protectedPersonSide)) ||
(preferPixelStructure ||
process.env.CANVAS_SCALE_ADJUST_ENABLE_RMBG_STRUCTURE === "1" ||
(includePersonPixels &&
process.env.CANVAS_SCALE_ADJUST_DISABLE_PERSON_RMBG !== "1"));
@ -3136,6 +3183,7 @@ async function buildScaleAdjustStructureLockedComposite(params: {
? null
: await buildScaleAdjustRmbgStructureLockedComposite({
placement,
preferPixelStructure,
protectedPersonSide,
raw,
sourceBuffer,
@ -3552,9 +3600,7 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
const preferPixelStructure = isScaleAdjustFurnitureProductLikePrompt(
region.prompt
);
const protectedPersonSide = preferPixelStructure
? getScaleAdjustProtectedPersonSide(region.prompt)
: null;
const protectedPersonSide = getScaleAdjustProtectedPersonSide(region.prompt);
const hasPersonPixels =
!preferPixelStructure &&
!!sourceRaw &&
@ -3761,19 +3807,35 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
width: sourceWidth,
})
: null;
const sourceOnlyRepairMaskBuffer = compositeClipComposites.length
? await buildScaleAdjustCompositeClipMaskBuffer({
height: sourceHeight,
sourceMaskComposites: compositeClipComposites,
targetMaskComposites: [],
width: sourceWidth,
})
: null;
const productLockBackgroundRepairMaskBuffer =
productLockComposites.length && sourceOnlyRepairMaskBuffer
? await buildScaleAdjustProductLockBackgroundRepairMaskBuffer({
productSourceMaskBuffer: sourceOnlyRepairMaskBuffer,
sourceHeight,
sourceWidth,
})
: null;
const productLockEditMaskBuffer =
productLockComposites.length && compositeClipMaskBuffer
productLockComposites.length && productLockBackgroundRepairMaskBuffer
? await buildScaleAdjustTransparentEditMaskFromOpacityMask({
height: sourceHeight,
maskBuffer: compositeClipMaskBuffer,
maskBuffer: productLockBackgroundRepairMaskBuffer,
width: sourceWidth,
})
: null;
const productLockFalEditMaskBuffer =
productLockComposites.length && compositeClipMaskBuffer
productLockComposites.length && productLockBackgroundRepairMaskBuffer
? await buildScaleAdjustOpaqueEditMaskFromOpacityMask({
height: sourceHeight,
maskBuffer: compositeClipMaskBuffer,
maskBuffer: productLockBackgroundRepairMaskBuffer,
width: sourceWidth,
})
: null;
@ -3782,6 +3844,7 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
backgroundPlateBuffer,
buffer,
compositeClipMaskBuffer,
productLockBackgroundRepairMaskBuffer,
productLockEditMaskBuffer,
productLockFalEditMaskBuffer,
productLockOverlayBuffer,
@ -3791,6 +3854,7 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
async function createScaleAdjustPatchReference(params: {
fullFrameReferenceOnly?: boolean;
minimalProductLockMaskEditReferences?: boolean;
regions: NormalizedLocalEditRegion[];
scalePercent?: ScaleAdjustPercent | null;
sourceBuffer: Buffer;
@ -3829,15 +3893,80 @@ async function createScaleAdjustPatchReference(params: {
const targetDraftBuffer = targetDraftReference.buffer;
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
if (params.fullFrameReferenceOnly && structureLockAppliedRef.value) {
if (
params.minimalProductLockMaskEditReferences &&
sourceImageUrl &&
targetDraftReference.productLockOverlayBuffer &&
targetDraftReference.productLockEditMaskBuffer
) {
const productLockOverlayUpload = await uploadFile(
`canvas-scale-adjust-product-lock-overlay-${suffix}.png`,
toArrayBuffer(targetDraftReference.productLockOverlayBuffer)
);
const productLockProtectMaskUpload = targetDraftReference.productLockProtectMaskBuffer
? await uploadFile(
`canvas-scale-adjust-product-lock-protect-mask-${suffix}.png`,
toArrayBuffer(targetDraftReference.productLockProtectMaskBuffer)
)
: null;
const productLockBackgroundRepairMaskUpload =
targetDraftReference.productLockBackgroundRepairMaskBuffer
? await uploadFile(
`canvas-scale-adjust-product-lock-background-repair-mask-${suffix}.png`,
toArrayBuffer(targetDraftReference.productLockBackgroundRepairMaskBuffer)
)
: null;
const productLockEditMaskUpload = await uploadFile(
`canvas-scale-adjust-product-lock-edit-mask-${suffix}.png`,
toArrayBuffer(targetDraftReference.productLockEditMaskBuffer)
);
const productLockFalEditMaskUpload = targetDraftReference.productLockFalEditMaskBuffer
? await uploadFile(
`canvas-scale-adjust-product-lock-fal-edit-mask-${suffix}.png`,
toArrayBuffer(targetDraftReference.productLockFalEditMaskBuffer)
)
: null;
return {
backgroundPlateFullUrl: null,
compositeClipMaskUrl: null,
productLockBackgroundRepairMaskUrl:
productLockBackgroundRepairMaskUpload?.url || null,
productLockMethod: "rmbg-cutout",
productLockEditMaskUrl: productLockEditMaskUpload.url,
productLockFalEditMaskUrl: productLockFalEditMaskUpload?.url || null,
productLockOverlayUrl: productLockOverlayUpload.url,
productLockProtectMaskUrl: productLockProtectMaskUpload?.url || null,
sourceCropUrl: sourceImageUrl,
sourceFullUrl: sourceImageUrl,
structureLockedDraft: structureLockAppliedRef.value,
targetDraftFullUrl: sourceImageUrl,
targetDraftUrl: sourceImageUrl,
workspacePixelRect,
workspaceRect,
};
}
const [
sourceFullUpload,
targetDraftFullUpload,
backgroundPlateFullUpload,
productLockOverlayUpload,
productLockProtectMaskUpload,
compositeClipMaskUpload,
productLockBackgroundRepairMaskUpload,
productLockEditMaskUpload,
productLockFalEditMaskUpload,
] = await Promise.all([
sharp(sourceBuffer, { failOn: "none" })
.png({ compressionLevel: 6 })
.toBuffer()
.then((sourceFullBuffer) =>
uploadFile(
`canvas-scale-adjust-source-full-${suffix}.png`,
toArrayBuffer(sourceFullBuffer)
)
),
uploadFile(
`canvas-scale-adjust-full-target-draft-${suffix}.png`,
toArrayBuffer(targetDraftBuffer)
@ -3866,6 +3995,12 @@ async function createScaleAdjustPatchReference(params: {
toArrayBuffer(targetDraftReference.compositeClipMaskBuffer)
)
: Promise.resolve(null),
targetDraftReference.productLockBackgroundRepairMaskBuffer
? uploadFile(
`canvas-scale-adjust-product-lock-background-repair-mask-${suffix}.png`,
toArrayBuffer(targetDraftReference.productLockBackgroundRepairMaskBuffer)
)
: Promise.resolve(null),
targetDraftReference.productLockEditMaskBuffer
? uploadFile(
`canvas-scale-adjust-product-lock-edit-mask-${suffix}.png`,
@ -3883,12 +4018,15 @@ async function createScaleAdjustPatchReference(params: {
return {
backgroundPlateFullUrl: backgroundPlateFullUpload?.url || null,
compositeClipMaskUrl: compositeClipMaskUpload?.url || null,
productLockBackgroundRepairMaskUrl:
productLockBackgroundRepairMaskUpload?.url || null,
productLockMethod: productLockOverlayUpload ? "rmbg-cutout" : null,
productLockEditMaskUrl: productLockEditMaskUpload?.url || null,
productLockFalEditMaskUrl: productLockFalEditMaskUpload?.url || null,
productLockOverlayUrl: productLockOverlayUpload?.url || null,
productLockProtectMaskUrl: productLockProtectMaskUpload?.url || null,
sourceCropUrl: sourceImageUrl || targetDraftFullUpload.url,
sourceFullUrl: sourceFullUpload.url,
structureLockedDraft: structureLockAppliedRef.value,
targetDraftFullUrl: targetDraftFullUpload.url,
targetDraftUrl: targetDraftFullUpload.url,
@ -3915,6 +4053,7 @@ async function createScaleAdjustPatchReference(params: {
productLockOverlayUpload,
productLockProtectMaskUpload,
compositeClipMaskUpload,
productLockBackgroundRepairMaskUpload,
productLockEditMaskUpload,
productLockFalEditMaskUpload,
] = await Promise.all([
@ -3954,6 +4093,12 @@ async function createScaleAdjustPatchReference(params: {
toArrayBuffer(targetDraftReference.compositeClipMaskBuffer)
)
: Promise.resolve(null),
targetDraftReference.productLockBackgroundRepairMaskBuffer
? uploadFile(
`canvas-scale-adjust-product-lock-background-repair-mask-${suffix}.png`,
toArrayBuffer(targetDraftReference.productLockBackgroundRepairMaskBuffer)
)
: Promise.resolve(null),
targetDraftReference.productLockEditMaskBuffer
? uploadFile(
`canvas-scale-adjust-product-lock-edit-mask-${suffix}.png`,
@ -3971,6 +4116,8 @@ async function createScaleAdjustPatchReference(params: {
return {
backgroundPlateFullUrl: backgroundPlateFullUpload?.url || null,
compositeClipMaskUrl: compositeClipMaskUpload?.url || null,
productLockBackgroundRepairMaskUrl:
productLockBackgroundRepairMaskUpload?.url || null,
productLockMethod: productLockOverlayUpload ? "rmbg-cutout" : null,
productLockEditMaskUrl: productLockEditMaskUpload?.url || null,
productLockFalEditMaskUrl: productLockFalEditMaskUpload?.url || null,
@ -4965,7 +5112,7 @@ function buildScaleAdjustProductLockBackgroundPrompt(
regions: NormalizedLocalEditRegion[],
scalePercent: ScaleAdjustPercent | null | undefined,
sourceSize: { width: number; height: number },
options: { draftInput?: boolean; maskEdit?: boolean } = {}
options: { draftInput?: boolean; guidedFinalInput?: boolean; maskEdit?: boolean } = {}
) {
const regionLines = regions
.map((region) => {
@ -4973,29 +5120,44 @@ function buildScaleAdjustProductLockBackgroundPrompt(
const targetPixelRect =
region.targetPixelRect ||
rectToPixels(targetRect, sourceSize.width, sourceSize.height);
return `Region ${region.index}: remove the selected furniture/product from the original area x=${region.pixelRect.left}, y=${region.pixelRect.top}, w=${region.pixelRect.width}, h=${region.pixelRect.height}; keep the final target footprint x=${targetPixelRect.left}, y=${targetPixelRect.top}, w=${targetPixelRect.width}, h=${targetPixelRect.height} visually clean and empty because the locked product pixels will be composited there after generation. User request: ${region.prompt}.`;
return `Region ${region.index}: the selected furniture/product originally occupies x=${region.pixelRect.left}, y=${region.pixelRect.top}, w=${region.pixelRect.width}, h=${region.pixelRect.height}. The final locked product will later occupy x=${targetPixelRect.left}, y=${targetPixelRect.top}, w=${targetPixelRect.width}, h=${targetPixelRect.height}. In this edit call, remove the old product and any old rectangular haze/ghost/plate inside the white mask; synthesize only the room background. User request: ${region.prompt}.`;
})
.join("\n");
return [
"Full-frame background repair for product-locked scale adjustment.",
"Return exactly one clean realistic full image with the same framing, camera, aspect ratio, and canvas extent as input image 1. Do not crop, zoom, pan, or resize the whole photo.",
options.maskEdit
options.guidedFinalInput
? "Full-frame guided cleanup for product-locked scale adjustment."
: "Full-frame masked background repair for product-locked scale adjustment.",
"Return exactly one clean realistic full image with the same framing, camera, aspect ratio, and canvas extent as input image 1. Do not crop, zoom, pan, resize, or redesign the whole photo.",
options.guidedFinalInput
? "Input image 1 is the current full photo. Input image 2 is a rough target-layout draft used only to identify where the locked product will be composited later. If an additional background-plate reference is provided, use it only as a cleanup hint for the old footprint."
: options.maskEdit
? options.draftInput
? "Input image 1 is a full-photo repair draft from the original photo. The old selected furniture/product footprint has been roughly wiped and the locked product may already appear near its final target footprint. The supplied edit mask marks the rough/old footprint area that must be repaired into natural background and seamless integration; do not edit outside the masked area."
: "Input image 1 is the original clean full photo. The supplied edit mask marks the old selected furniture/product footprint that must be repaired as natural background; do not edit outside the masked area."
? "Input image 1 is a full-photo rough background plate from the original photo. The old selected furniture/product footprint has been roughly wiped, and the target product is intentionally absent. The supplied edit mask marks only that rough old footprint area; repair it into natural background and do not edit outside the masked area."
: "Input image 1 is the current full photo. The supplied edit mask is the only allowed edit area: white or transparent pixels must be repaired; black/opaque pixels outside the mask must be copied exactly from input image 1."
: "Input image 1 is the original clean full photo. Input image 2 is a background plate reference that removes the selected furniture/product footprint and keeps the room/person/background layout stable.",
"Important: do not draw, recreate, move, scale, duplicate, or redesign the selected furniture/product in the final image. The system will composite the locked original product pixels into the final target footprint after your background image is returned.",
options.guidedFinalInput
? "Important: return a background plate only. Use input image 2 only to understand the target placement, but do not render the selected furniture/product at either the old footprint or target footprint. The system will composite the locked original product pixels after your image is returned, so prioritize natural background, contact shadow, and clean fusion."
: "Important: do not draw, recreate, move, scale, duplicate, or redesign the selected furniture/product in the returned image. The returned image must be a background plate only; the system will composite the locked original product pixels into the final target footprint after your background image is returned.",
options.draftInput
? "Your job is only to turn the rough wiped footprint and any visible draft haze, gray wash, ghost product silhouette, or edge patch into natural wall, curtain, floor, rug, shadow, sofa edge, and background. If the locked product is visible in the draft, keep the surrounding background and contact shadows natural, but do not invent a second product or preserve old-size remnants."
: "Your job is only to erase the old selected furniture/product footprint, complete newly exposed wall, curtain, floor, rug, shadow, sofa edge, and background naturally, and keep the target footprint clean enough for the locked product layer to sit there.",
? "Your job is only to turn the rough wiped footprint and any visible draft haze, gray wash, ghost product silhouette, or edge patch into natural wall, curtain, floor, rug, shadow, sofa edge, and background. Keep the target footprint empty and clean for the locked product layer; do not invent a product, second product, or old-size remnant."
: options.guidedFinalInput
? "Your job is to clean the rough target-layout draft into a natural product-free background plate: erase the old selected furniture/product from its original location, remove translucent old-size silhouettes and rectangular haze/panels, complete the exposed wall, curtain, floor, rug, shadows, sofa edge, and background, and keep the target footprint empty for the locked product layer."
: "Your job is only to erase the old selected furniture/product, erase any translucent old-size silhouette, erase any rectangular haze/panel/plate left by previous edits, and complete newly exposed wall, curtain, floor, rug, shadow, sofa edge, and background naturally. Leave no placeholder where the product used to be.",
options.guidedFinalInput
? "Do not copy rough draft haze, wash blocks, hard mask shapes, pasted edges, or the rough target product from the target-layout draft. It is a placement hint, not final content."
: options.maskEdit
? "The mask may be wider than the object silhouette because it also covers old edit artifacts. Do not interpret the mask shape as a box or object to render. Fill the entire editable white/transparent area with continuous real room background."
: "Use the background plate reference only as a repair guide, not as visible final content.",
"Preserve all non-target people exactly as foreground/background context: do not move, cover, deform, redraw, change face, change body, change hands, change legs, change clothes, or change pose.",
"Keep all non-target content stable: sofa, window, curtain, cabinet, lamp, wall art, floor, rug, lighting, textural grain, and global color must remain consistent.",
"Keep all non-target content stable outside the editable mask: sofa, window, curtain, cabinet, lamp, wall art, floor, rug, lighting, textural grain, and global color must remain consistent.",
options.maskEdit
? "The edit mask is only an invisible repair boundary. Do not output pale wash blocks, rough mask shapes, hard rectangular edges, black holes, ghost fragments, pasted silhouettes, or old-size residues."
? "The edit mask is only an invisible repair permission map. Do not output pale wash blocks, rough mask shapes, hard rectangular edges, black holes, ghost fragments, pasted silhouettes, old-size residues, or a gray copy of the removed product."
: "Remove draft artifacts from input image 2: pale wash blocks, rough masks, missing patches, hard rectangular edges, black holes, ghost fragments, pasted silhouettes, and old-size residues. They are only edit guides, not final content.",
"Do not output boxes, masks, labels, coordinate marks, UI marks, arrows, guide overlays, crop borders, or any annotation artifacts.",
"中文硬约束:这是产品结构锁定流程。你只生成背景底图,不要生成、移动、缩放或重绘目标家具/商品;目标家具/商品会在你输出后由系统用原图锁定像素叠回目标位置。必须擦除旧位置的目标家具/商品并自然补全墙面、窗帘、地面、阴影和背景。非目标人物必须保持原样,不能变脸、变形、改变衣服、手、腿、脚或姿势。最终背景图不能有矩形雾块、贴片边、黑洞、残影、标注框,也不能出现第二个目标家具/商品。",
options.guidedFinalInput
? "中文硬约束:这是产品结构锁定流程。输入2只是目标位置草稿,不能把草稿的灰雾、贴片边、硬边、旧残影或目标产品带到最终图。你返回的是无产品背景底图:旧位置和目标位置都不能出现目标家具/商品,只能自然补全墙面、窗帘、地面、阴影和背景。目标家具/商品最终会由系统用原图锁定像素覆盖,所以你不要生成产品本体;重点做好背景补全、接触阴影和融合。非目标人物必须保持原样,不能变脸、变形、改变衣服、手、腿、脚或姿势。最终图不能有矩形雾块、贴片边、黑洞、残影、标注框,也不能出现第二个目标家具/商品。"
: "中文硬约束:这是产品结构锁定流程。你只生成背景底图,不要生成、移动、缩放或重绘目标家具/商品;目标家具/商品会在你输出后由系统用原图锁定像素叠回目标位置。白色/透明 mask 内必须清空旧产品、旧尺寸残影、灰色雾块、矩形贴片和旧标注痕迹,只补成连续自然的墙面、窗帘、地面、阴影和背景。黑色/不透明 mask 外必须保持原图不变。非目标人物必须保持原样,不能变脸、变形、改变衣服、手、腿、脚或姿势。最终背景图不能有矩形雾块、贴片边、黑洞、残影、标注框,也不能出现第二个目标家具/商品。",
"",
"Region instructions:",
regionLines,
@ -5136,7 +5298,9 @@ export async function POST(request: NextRequest) {
const preferScaleAdjustFalNanoBananaEdit =
hasScaleAdjustShrink && !hasScaleAdjustEnlarge;
const preferScaleAdjustProviderTypes =
hasScaleAdjustShrink && !hasScaleAdjustEnlarge ? ["grsai-gpt-image"] : [];
hasScaleAdjustShrink && !hasScaleAdjustEnlarge
? ["grsai-gpt-image", "fal-image"]
: [];
const displayPrompt = buildLocalEditDisplayPrompt({ operation, scalePercent });
const resolvedAspectRatio = resolveLocalEditAspectRatio(
body.aspectRatio,
@ -5314,16 +5478,33 @@ export async function POST(request: NextRequest) {
regions.some((region) =>
isScaleAdjustFurnitureProductLikePrompt(region.prompt)
),
minimalProductLockMaskEditReferences:
useScaleAdjustStructureLockedPatchWorkflow &&
isScaleAdjustMaskCapableModel(resolvedScaleAdjustModelEntry) &&
regions.some((region) =>
isScaleAdjustFurnitureProductLikePrompt(region.prompt)
),
userId: session.user.id,
});
} catch (error) {
scaleAdjustPatchReferenceFailed = true;
console.warn(
"[Canvas Local Edit] Scale adjust patch reference failed; falling back to source-only coordinate prompt:",
"[Canvas Local Edit] Scale adjust patch reference failed:",
{
error: error instanceof Error ? error.message : String(error),
structureLockedPatchWorkflow: useScaleAdjustStructureLockedPatchWorkflow,
}
);
if (useScaleAdjustStructureLockedPatchWorkflow) {
return NextResponse.json(
{
success: false,
error:
"比例调整参考图上传失败,请稍后重试;已停止本次生成以避免返回无效结果。",
},
{ status: 503 }
);
}
}
}
const shouldCreateScaleAdjustCropReferences =
@ -5349,14 +5530,24 @@ export async function POST(request: NextRequest) {
regions.some((region) =>
isScaleAdjustFurnitureProductLikePrompt(region.prompt)
);
const useScaleAdjustProductLockComposite =
const useScaleAdjustProductLockBase =
useScaleAdjustFullFrameStructurePatch &&
Boolean(scaleAdjustPatchReference?.backgroundPlateFullUrl) &&
Boolean(scaleAdjustPatchReference?.productLockOverlayUrl);
const useScaleAdjustProductLockMaskEdit =
useScaleAdjustProductLockComposite &&
useScaleAdjustProductLockBase &&
isScaleAdjustMaskCapableModel(resolvedScaleAdjustModelEntry) &&
Boolean(scaleAdjustPatchReference?.productLockEditMaskUrl);
const useScaleAdjustProductLockComposite =
useScaleAdjustProductLockBase &&
(Boolean(scaleAdjustPatchReference?.backgroundPlateFullUrl) ||
useScaleAdjustProductLockMaskEdit);
const useScaleAdjustProductLockDirectMaskPlate =
useScaleAdjustProductLockComposite && useScaleAdjustProductLockMaskEdit;
const useScaleAdjustProductLockGuidedFinal =
useScaleAdjustProductLockComposite &&
!useScaleAdjustProductLockMaskEdit &&
process.env.CANVAS_SCALE_ADJUST_ENABLE_PRODUCT_LOCK_GUIDED_FINAL === "1" &&
Boolean(scaleAdjustPatchReference?.targetDraftFullUrl);
const useScaleAdjustPatchTargetDraftPrimary = Boolean(scaleAdjustPatchReference);
const includeScaleAdjustPatchSourceReference =
Boolean(scaleAdjustPatchReference);
@ -5373,7 +5564,7 @@ export async function POST(request: NextRequest) {
scalePercent,
{ width: sourceWidth, height: sourceHeight },
{
draftInput: useScaleAdjustProductLockMaskEdit,
guidedFinalInput: useScaleAdjustProductLockGuidedFinal,
maskEdit: useScaleAdjustProductLockMaskEdit,
}
)
@ -5436,6 +5627,8 @@ export async function POST(request: NextRequest) {
"boxes, labels, UI marks, arrows, text, overlays, seams, rectangular patch edge, haze block, ghost copy, old-size subject, unchanged subject, duplicate subject, stretched subject, distorted product, changed product structure, changed person, changed face, deformed person, extra limbs, changed hands, changed feet, whole-image zoom, crop, pan";
const scaleAdjustProductLockBackgroundNegativePrompt =
"selected furniture, selected product, moved furniture, scaled furniture, recreated product, duplicate furniture, duplicate product, old-size furniture, target object visible, product ghost, product remnant, boxes, labels, UI marks, arrows, text, overlays, seams, rectangular patch edge, haze block, black hole, changed person, changed face, deformed person, extra limbs, changed hands, changed feet, whole-image zoom, crop, pan";
const scaleAdjustProductLockGuidedNegativePrompt =
"selected furniture, selected product, target furniture visible, target product visible, old-size furniture, old-position furniture, duplicate furniture, duplicate product, second copy, product ghost, product remnant, pasted edge, rectangular patch edge, rectangular haze block, gray wash block, black hole, boxes, labels, UI marks, arrows, text, overlays, changed person, changed face, deformed person, extra limbs, changed hands, changed feet, whole-image zoom, crop, pan";
const manualRegionReferenceImageUrls = regions.flatMap((region) =>
region.referenceImages.map((reference) => reference.url)
);
@ -5448,17 +5641,26 @@ export async function POST(request: NextRequest) {
? useScaleAdjustProductLockComposite
? useScaleAdjustProductLockMaskEdit
? [
scaleAdjustPatchReference.targetDraftFullUrl ||
sourceImageUrl,
scaleAdjustPatchReference.sourceFullUrl || sourceImageUrl,
...scaleAdjustCropReferenceImageUrls,
...manualRegionReferenceImageUrls,
]
: useScaleAdjustProductLockGuidedFinal
? [
scaleAdjustPatchReference.sourceFullUrl || sourceImageUrl,
scaleAdjustPatchReference.targetDraftFullUrl as string,
...(scaleAdjustPatchReference.backgroundPlateFullUrl
? [scaleAdjustPatchReference.backgroundPlateFullUrl]
: []),
...scaleAdjustCropReferenceImageUrls,
...manualRegionReferenceImageUrls,
]
: [
sourceImageUrl,
scaleAdjustPatchReference.backgroundPlateFullUrl as string,
...scaleAdjustCropReferenceImageUrls,
...manualRegionReferenceImageUrls,
]
sourceImageUrl,
scaleAdjustPatchReference.backgroundPlateFullUrl as string,
...scaleAdjustCropReferenceImageUrls,
...manualRegionReferenceImageUrls,
]
: [
sourceImageUrl,
scaleAdjustPatchReference.targetDraftFullUrl as string,
@ -5503,6 +5705,12 @@ export async function POST(request: NextRequest) {
isScaleAdjustOperation && scaleAdjustPatchReference
? scaleAdjustProtectedChangeRegions
: scaleAdjustCompositeRegions;
const scaleAdjustProductLockRepairClipMaskUrl =
useScaleAdjustProductLockComposite && useScaleAdjustProductLockMaskEdit
? scaleAdjustPatchReference?.productLockBackgroundRepairMaskUrl ||
scaleAdjustPatchReference?.compositeClipMaskUrl ||
undefined
: scaleAdjustPatchReference?.compositeClipMaskUrl || undefined;
const scaleAdjustVerificationConfig = isScaleAdjustOperation
? {
allowOutsideChangedWhenChangedInside: false,
@ -5607,7 +5815,9 @@ export async function POST(request: NextRequest) {
scaleAdjustPatchReferenceOrder:
scaleAdjustPatchReference && useScaleAdjustFullFrameStructurePatch
? useScaleAdjustProductLockMaskEdit
? "full-source-product-lock-edit-mask"
? "product-lock-source-edit-mask"
: useScaleAdjustProductLockGuidedFinal
? "product-lock-source-target-draft-guide"
: "full-source-full-target-draft"
: scaleAdjustPatchReference && useScaleAdjustPatchTargetDraftPrimary
? includeScaleAdjustPatchSourceReference
@ -5618,6 +5828,8 @@ export async function POST(request: NextRequest) {
: null,
scaleAdjustPatchSourceCropUrl:
scaleAdjustPatchReference?.sourceCropUrl || null,
scaleAdjustPatchSourceFullUrl:
scaleAdjustPatchReference?.sourceFullUrl || null,
scaleAdjustPatchTargetDraftUrl:
scaleAdjustPatchReference?.targetDraftUrl || null,
scaleAdjustPatchTargetDraftFullUrl:
@ -5630,12 +5842,16 @@ export async function POST(request: NextRequest) {
scaleAdjustPatchReference?.productLockMethod || null,
scaleAdjustProductLockMaskEditApplied:
useScaleAdjustProductLockMaskEdit,
scaleAdjustProductLockGuidedFinalApplied:
useScaleAdjustProductLockGuidedFinal,
scaleAdjustRequiresProviderMaskInput:
useScaleAdjustProductLockMaskEdit,
scaleAdjustProductLockEditMaskUrl:
scaleAdjustPatchReference?.productLockEditMaskUrl || null,
scaleAdjustProductLockFalEditMaskUrl:
scaleAdjustPatchReference?.productLockFalEditMaskUrl || null,
scaleAdjustProductLockBackgroundRepairMaskUrl:
scaleAdjustPatchReference?.productLockBackgroundRepairMaskUrl || null,
scaleAdjustProductLockOverlayUrl:
scaleAdjustPatchReference?.productLockOverlayUrl || null,
scaleAdjustProductLockProtectMaskUrl:
@ -5699,7 +5915,9 @@ export async function POST(request: NextRequest) {
? scaleAdjustPatchReference
? useScaleAdjustProductLockComposite
? useScaleAdjustProductLockMaskEdit
? "product-lock-mask-edit-rmbg-source-patch-one-shot"
? "product-lock-source-mask-edit-rmbg-one-shot"
: useScaleAdjustProductLockGuidedFinal
? "product-lock-target-draft-guided-background-one-shot"
: "product-lock-background-plate-rmbg-v2-source-patch-one-shot"
: useScaleAdjustFullFrameStructurePatch
? "full-frame-source-locked-target-guide-v8-one-shot"
@ -5746,7 +5964,9 @@ export async function POST(request: NextRequest) {
? scaleAdjustPatchReference
? useScaleAdjustFullFrameStructurePatch
? useScaleAdjustProductLockComposite
? scaleAdjustProductLockBackgroundNegativePrompt
? useScaleAdjustProductLockGuidedFinal
? scaleAdjustProductLockGuidedNegativePrompt
: scaleAdjustProductLockBackgroundNegativePrompt
: scaleAdjustFullFramePatchNegativePrompt
: useFalScaleAdjustPatchPrompt
? scaleAdjustFalPatchNegativePrompt
@ -5771,22 +5991,22 @@ export async function POST(request: NextRequest) {
? { maskImageUrl: scaleAdjustEditMaskUpload.url }
: undefined,
referenceImages,
scaleAdjustPatchComposite: useScaleAdjustProductLockComposite
scaleAdjustPatchComposite:
useScaleAdjustProductLockComposite
? {
diffAlphaCutoff: 18,
diffAlphaMultiplier: 2.6,
diffCompareBlurPx: 1.1,
diffHighThreshold: 64,
diffLowThreshold: 20,
featherMaxPx: 34,
featherMinPx: 8,
featherRatio: 0.015,
featherMaxPx: useScaleAdjustProductLockDirectMaskPlate ? 72 : 34,
featherMinPx: useScaleAdjustProductLockDirectMaskPlate ? 18 : 8,
featherRatio: useScaleAdjustProductLockDirectMaskPlate ? 0.026 : 0.015,
generatedFrame: "full",
maskMode: "diff",
maskMode: useScaleAdjustProductLockDirectMaskPlate ? "workspace" : "diff",
regions: scaleAdjustPatchCompositeRegions,
sourceImageUrl,
clipMaskImageUrl:
scaleAdjustPatchReference?.compositeClipMaskUrl || undefined,
clipMaskImageUrl: scaleAdjustProductLockRepairClipMaskUrl,
workspaceRegion:
(scaleAdjustPatchReference as ScaleAdjustPatchReference)
.workspaceRect,

View File

@ -1976,7 +1976,7 @@ export default function CanvasEditorLegacyPage({
);
const [activeWorkflowExecutionTaskKey, setActiveWorkflowExecutionTaskKey] =
useState<string | null>(null);
const [isPanelOpen, setIsPanelOpen] = useState(() => !isVideoMode);
const [isPanelOpen, setIsPanelOpen] = useState(() => requestedAgentMode);
const [floatingPanelWidth, setFloatingPanelWidth] =
useState(FLOATING_PANEL_WIDTH);
const [
@ -3280,11 +3280,15 @@ export default function CanvasEditorLegacyPage({
}, [checkCanvasAuthSession]);
useEffect(() => {
if (!isPanelOpen && !isVideoCanvasFileListOpen) {
return;
}
const timeoutId = window.setTimeout(() => {
setAreSecondaryPanelsReady(true);
}, 800);
}, 120);
return () => window.clearTimeout(timeoutId);
}, []);
}, [isPanelOpen, isVideoCanvasFileListOpen]);
const hydrateCanvasProject = useCallback(
(project: CanvasInitialProject) => {
@ -10844,7 +10848,7 @@ export default function CanvasEditorLegacyPage({
</div>
) : null}
<div className="flex h-full w-full flex-col overflow-hidden border-l bg-white dark:bg-zinc-950">
{!areSecondaryPanelsReady ? (
{!isPanelOpen ? null : !areSecondaryPanelsReady ? (
<CanvasLazyPanelLoader label="正在准备侧边面板..." />
) : panelMode === "chat" ? (
// AI 对话内容 - 使用 key 强制在 id 变化时重新挂载

View File

@ -2,7 +2,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { signIn, signOut } from "next-auth/react";
import { useTheme } from "next-themes";
import useSWR from "swr";
import useSWRInfinite from "swr/infinite";
@ -60,7 +59,6 @@ import {
getCanvasLaunchStorageKey,
} from "@/lib/canvas-launch";
import type { MembershipConfig } from "@/lib/membership/config";
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
import {
normalizeLovartHomeSidebarConfig,
type LovartHomeSidebarItemKey,
@ -465,6 +463,7 @@ export function LovartHome({
const [previewCursorVisible, setPreviewCursorVisible] = useState(true);
const [isSecondaryDataReady, setIsSecondaryDataReady] = useState(false);
const [isFeaturedDataReady, setIsFeaturedDataReady] = useState(false);
const [isHomeAnimationReady, setIsHomeAnimationReady] = useState(false);
const projectSectionRef = useRef<HTMLDivElement | null>(null);
const featuredSectionRef = useRef<HTMLElement | null>(null);
const featuredLoadMoreRef = useRef<HTMLDivElement | null>(null);
@ -535,6 +534,31 @@ export function LovartHome({
};
}, [isFeaturedDataReady]);
useEffect(() => {
if (typeof window === "undefined" || !isSecondaryDataReady) {
return;
}
const loadHomeAnimation = () => setIsHomeAnimationReady(true);
const idleWindow = window as Window & {
cancelIdleCallback?: (handle: number) => void;
requestIdleCallback?: (
callback: IdleRequestCallback,
options?: IdleRequestOptions
) => number;
};
if (typeof idleWindow.requestIdleCallback === "function") {
const idleId = idleWindow.requestIdleCallback(loadHomeAnimation, {
timeout: 3200,
});
return () => idleWindow.cancelIdleCallback?.(idleId);
}
const timer = window.setTimeout(loadHomeAnimation, 1800);
return () => window.clearTimeout(timer);
}, [isSecondaryDataReady]);
const { data: canvasData, mutate: mutateCanvas } = useSWR<{
success: boolean;
data: CanvasProject[];
@ -671,7 +695,10 @@ export function LovartHome({
enabled?: boolean;
htmlContent?: string;
fileName?: string;
}>(isSecondaryDataReady ? "/api/home-animation" : null, fetcher);
}>(
isSecondaryDataReady && isHomeAnimationReady ? "/api/home-animation" : null,
fetcher
);
const membershipConfig = membershipData?.config;
const membershipPlans = membershipConfig?.plans || [];
@ -896,6 +923,7 @@ export function LovartHome({
} catch (_error) {}
try {
const { signOut } = await import("next-auth/react");
await signOut({
callbackUrl: "/",
redirect: false,
@ -1109,6 +1137,9 @@ export function LovartHome({
};
const uploadSelectedFiles = async (): Promise<CanvasLaunchAttachment[]> => {
const { fetchUploadWithRetry } = await import(
"@/lib/client/upload-with-retry"
);
const result: CanvasLaunchAttachment[] = [];
for (const item of files) {
@ -1146,6 +1177,7 @@ export function LovartHome({
setIsLoggingIn(true);
try {
const { signIn } = await import("next-auth/react");
const result = await signIn("credentials", {
email,
password: loginPassword,
@ -1171,6 +1203,7 @@ export function LovartHome({
toast.error("Google 登录未启用");
return;
}
const { signIn } = await import("next-auth/react");
await signIn("google", { callbackUrl: "/" });
};
@ -1226,6 +1259,9 @@ export function LovartHome({
const blob = await response.blob();
const formData = new FormData();
formData.append("file", blob, `edited-${Date.now()}.png`);
const { fetchUploadWithRetry } = await import(
"@/lib/client/upload-with-retry"
);
const uploadResponse = await fetchUploadWithRetry(formData);
if (!uploadResponse.ok) {
@ -1802,7 +1838,9 @@ export function LovartHome({
heroPromoText={heroPromoText}
heroSubtitleText={heroSubtitleText}
heroTitleText={heroTitleText}
homeAnimationData={homeAnimationData}
homeAnimationData={
isHomeAnimationReady ? homeAnimationData : undefined
}
isAgentModeActive={isComposerAgentModeActive}
isMembershipEnabled={isMembershipEnabled}
isSubmitting={isSubmitting}

View File

@ -62,6 +62,8 @@ export function LovartFeaturedGallery({
<img
src={item.thumbnailUrl || item.originalUrl}
alt={item.prompt || "灵感图"}
loading="lazy"
decoding="async"
className="block h-auto w-full rounded-[6px] bg-[#e8e8e8] object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-[1.01] dark:bg-[#1b1c20]"
/>
<div className="mt-[8px] flex h-[22px] items-center justify-between text-[11px] font-normal leading-none text-[#b8b8b8] dark:text-white/45">

View File

@ -1,6 +1,6 @@
import dynamic from "next/dynamic";
import { ArrowUp, Box, Lightbulb, Loader2, Plus, Sparkles } from "lucide-react";
import type { ChangeEvent } from "react";
import { HomeAnimationEmbed } from "@/components/home/home-animation-embed";
import { Button } from "@/components/ui/button";
import {
Select,
@ -12,6 +12,19 @@ import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import type { ModelOption, UploadFileItem } from "./types";
const HomeAnimationEmbed = dynamic(
() =>
import("@/components/home/home-animation-embed").then(
(module) => module.HomeAnimationEmbed
),
{
loading: () => (
<div className="h-[360px] w-full rounded-[24px] border border-black/10 bg-black/[0.03] md:h-[420px] dark:border-white/10 dark:bg-white/[0.04]" />
),
ssr: false,
}
);
type LovartHeroComposerProps = {
composerPlaceholder: string;
displayLogo: string;

View File

@ -76,6 +76,8 @@ export function LovartProjectsSection({
<img
src={item.thumbnail}
alt={item.title}
loading="lazy"
decoding="async"
className="h-full w-full object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-[1.025]"
/>
) : (

View File

@ -695,6 +695,7 @@ function buildFalRequestPayload(params: {
normalizeOptionalHttpUrl(params.imageParams?.falMaskImageUrl) ||
normalizeOptionalHttpUrl(params.imageParams?.maskImageUrl);
if (maskImageUrl) {
input.mask_image_url = maskImageUrl;
input.mask_url = maskImageUrl;
}

View File

@ -3059,6 +3059,13 @@ async function applyScaleAdjustPatchCompositeToImages(params: {
touchesTop,
width: workspaceBox.width,
});
const staticPatchMaskRaw = staticPatchMask
? await normalizeAlphaMaskToRaw({
height: workspaceBox.height,
maskBuffer: staticPatchMask,
width: workspaceBox.width,
})
: null;
const regionClipPatchMask = maskRegionBoxes?.length
? buildScaleAdjustPatchCompositeMaskSvg({
featherPx,
@ -3177,11 +3184,21 @@ async function applyScaleAdjustPatchCompositeToImages(params: {
imageBuffer: resizedGeneratedBuffer,
width: workspaceBox.width,
})
: await sharp(resizedGeneratedBuffer, { failOn: "none" })
.ensureAlpha()
.composite([{ input: patchMask, blend: "dest-in" }])
.png({ compressionLevel: 6 })
.toBuffer();
: activeClipPatchMaskRaw || staticPatchMaskRaw
? await applyRawAlphaToImage({
alphaMaskRaw:
activeClipPatchMaskRaw && staticPatchMaskRaw
? multiplyAlphaMasks(staticPatchMaskRaw, activeClipPatchMaskRaw)
: activeClipPatchMaskRaw || staticPatchMaskRaw!,
height: workspaceBox.height,
imageBuffer: resizedGeneratedBuffer,
width: workspaceBox.width,
})
: await sharp(resizedGeneratedBuffer, { failOn: "none" })
.ensureAlpha()
.composite([{ input: patchMask, blend: "dest-in" }])
.png({ compressionLevel: 6 })
.toBuffer();
const mergedBuffer = await sharp(sourceBuffer, { failOn: "none" })
.ensureAlpha()
.composite([

View File

@ -45,7 +45,7 @@ export type StorageUploadFunction = (
let lastKnownGoodDbS3Config: S3UploadConfig | null = null;
const STORAGE_FAILOVER_CONFIG_KEY = "storage_failover_config";
const DEFAULT_S3_REQUEST_TIMEOUT_MS = 30_000;
const DEFAULT_S3_REQUEST_TIMEOUT_MS = 180_000;
function getStorageTimeoutMs(name: string, fallback: number, min: number, max: number) {
const parsed = Number(process.env[name]);