diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 7ee9fb4b8..6e4ad5593 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -128,6 +128,7 @@ type ScaleAdjustPatchReference = { compositeClipMaskUrl?: string | null; productLockMethod?: string | null; productLockEditMaskUrl?: string | null; + productLockFalEditMaskUrl?: string | null; productLockOverlayUrl?: string | null; productLockProtectMaskUrl?: string | null; sourceCropUrl: string; @@ -2262,6 +2263,36 @@ async function buildScaleAdjustTransparentEditMaskFromOpacityMask(params: { .toBuffer(); } +async function buildScaleAdjustOpaqueEditMaskFromOpacityMask(params: { + height: number; + maskBuffer: Buffer; + width: number; +}) { + const { height, maskBuffer, width } = params; + const editAlpha = await sharp(maskBuffer, { failOn: "none" }) + .rotate() + .resize(width, height, { fit: "fill" }) + .ensureAlpha() + .extractChannel(3) + .greyscale() + .raw() + .toBuffer(); + const rgba = Buffer.alloc(width * height * 4); + for (let index = 0; index < width * height; index += 1) { + const offset = index * 4; + const value = editAlpha[index] || 0; + rgba[offset] = value; + rgba[offset + 1] = value; + rgba[offset + 2] = value; + rgba[offset + 3] = 255; + } + return sharp(rgba, { + raw: { channels: 4, height, width }, + }) + .png({ compressionLevel: 9 }) + .toBuffer(); +} + function getScaleAdjustAlphaBoundingBox(params: { height: number; rawRgba: Buffer; @@ -3738,12 +3769,21 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: { width: sourceWidth, }) : null; + const productLockFalEditMaskBuffer = + productLockComposites.length && compositeClipMaskBuffer + ? await buildScaleAdjustOpaqueEditMaskFromOpacityMask({ + height: sourceHeight, + maskBuffer: compositeClipMaskBuffer, + width: sourceWidth, + }) + : null; return { backgroundPlateBuffer, buffer, compositeClipMaskBuffer, productLockEditMaskBuffer, + productLockFalEditMaskBuffer, productLockOverlayBuffer, productLockProtectMaskBuffer, }; @@ -3796,6 +3836,7 @@ async function createScaleAdjustPatchReference(params: { productLockProtectMaskUpload, compositeClipMaskUpload, productLockEditMaskUpload, + productLockFalEditMaskUpload, ] = await Promise.all([ uploadFile( `canvas-scale-adjust-full-target-draft-${suffix}.png`, @@ -3831,6 +3872,12 @@ async function createScaleAdjustPatchReference(params: { toArrayBuffer(targetDraftReference.productLockEditMaskBuffer) ) : Promise.resolve(null), + targetDraftReference.productLockFalEditMaskBuffer + ? uploadFile( + `canvas-scale-adjust-product-lock-fal-edit-mask-${suffix}.png`, + toArrayBuffer(targetDraftReference.productLockFalEditMaskBuffer) + ) + : Promise.resolve(null), ]); return { @@ -3838,6 +3885,7 @@ async function createScaleAdjustPatchReference(params: { compositeClipMaskUrl: compositeClipMaskUpload?.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, @@ -3868,6 +3916,7 @@ async function createScaleAdjustPatchReference(params: { productLockProtectMaskUpload, compositeClipMaskUpload, productLockEditMaskUpload, + productLockFalEditMaskUpload, ] = await Promise.all([ uploadFile( `canvas-scale-adjust-workspace-source-${suffix}.png`, @@ -3911,6 +3960,12 @@ async function createScaleAdjustPatchReference(params: { toArrayBuffer(targetDraftReference.productLockEditMaskBuffer) ) : Promise.resolve(null), + targetDraftReference.productLockFalEditMaskBuffer + ? uploadFile( + `canvas-scale-adjust-product-lock-fal-edit-mask-${suffix}.png`, + toArrayBuffer(targetDraftReference.productLockFalEditMaskBuffer) + ) + : Promise.resolve(null), ]); return { @@ -3918,6 +3973,7 @@ async function createScaleAdjustPatchReference(params: { compositeClipMaskUrl: compositeClipMaskUpload?.url || null, productLockMethod: productLockOverlayUpload ? "rmbg-cutout" : null, productLockEditMaskUrl: productLockEditMaskUpload?.url || null, + productLockFalEditMaskUrl: productLockFalEditMaskUpload?.url || null, productLockOverlayUrl: productLockOverlayUpload?.url || null, productLockProtectMaskUrl: productLockProtectMaskUpload?.url || null, sourceCropUrl: sourceCropUpload.url, @@ -5316,9 +5372,6 @@ export async function POST(request: NextRequest) { scalePercent, { width: sourceWidth, height: sourceHeight }, { - backgroundReference: - useScaleAdjustProductLockMaskEdit && - Boolean(scaleAdjustPatchReference?.backgroundPlateFullUrl), maskEdit: useScaleAdjustProductLockMaskEdit, } ) @@ -5394,9 +5447,6 @@ export async function POST(request: NextRequest) { ? useScaleAdjustProductLockMaskEdit ? [ sourceImageUrl, - ...(scaleAdjustPatchReference.backgroundPlateFullUrl - ? [scaleAdjustPatchReference.backgroundPlateFullUrl] - : []), ...scaleAdjustCropReferenceImageUrls, ...manualRegionReferenceImageUrls, ] @@ -5581,6 +5631,8 @@ export async function POST(request: NextRequest) { useScaleAdjustProductLockMaskEdit, scaleAdjustProductLockEditMaskUrl: scaleAdjustPatchReference?.productLockEditMaskUrl || null, + scaleAdjustProductLockFalEditMaskUrl: + scaleAdjustPatchReference?.productLockFalEditMaskUrl || null, scaleAdjustProductLockOverlayUrl: scaleAdjustPatchReference?.productLockOverlayUrl || null, scaleAdjustProductLockProtectMaskUrl: @@ -5706,6 +5758,9 @@ export async function POST(request: NextRequest) { promptExtend: false, providerImageParams: useScaleAdjustProductLockMaskEdit ? { + falMaskImageUrl: + scaleAdjustPatchReference?.productLockFalEditMaskUrl || + (scaleAdjustPatchReference?.productLockEditMaskUrl as string), maskImageUrl: scaleAdjustPatchReference?.productLockEditMaskUrl as string, } diff --git a/lib/ai/providers/fal-image.ts b/lib/ai/providers/fal-image.ts index b4e8f37fc..930815fe8 100644 --- a/lib/ai/providers/fal-image.ts +++ b/lib/ai/providers/fal-image.ts @@ -21,6 +21,7 @@ interface FalImageParams { safetyTolerance?: string | number; seed?: string | number; enableWebSearch?: boolean; + falMaskImageUrl?: string; quality?: string; maskImageUrl?: string; openaiApiKey?: string; @@ -690,7 +691,9 @@ function buildFalRequestPayload(params: { output_format: outputFormat, }; - const maskImageUrl = normalizeOptionalHttpUrl(params.imageParams?.maskImageUrl); + const maskImageUrl = + normalizeOptionalHttpUrl(params.imageParams?.falMaskImageUrl) || + normalizeOptionalHttpUrl(params.imageParams?.maskImageUrl); if (maskImageUrl) { input.mask_url = maskImageUrl; }