Compare commits

...

10 Commits

8 changed files with 213 additions and 170 deletions

View File

@ -471,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;
}
@ -2297,15 +2313,11 @@ async function buildScaleAdjustOpaqueEditMaskFromOpacityMask(params: {
async function buildScaleAdjustProductLockBackgroundRepairMaskBuffer(params: {
productSourceMaskBuffer: Buffer;
regions: NormalizedLocalEditRegion[];
scalePercent?: ScaleAdjustPercent | null;
sourceHeight: number;
sourceWidth: number;
}) {
const {
productSourceMaskBuffer,
regions,
scalePercent,
sourceHeight,
sourceWidth,
} = params;
@ -2318,141 +2330,16 @@ async function buildScaleAdjustProductLockBackgroundRepairMaskBuffer(params: {
.raw()
.toBuffer();
const minSide = Math.max(1, Math.min(sourceWidth, sourceHeight));
const blur = Math.max(8, Math.min(34, Math.round(minSide * 0.018)));
const repairRects: string[] = [];
const protectRects: string[] = [];
for (const region of regions) {
const targetRect = getScaleAdjustTargetRect(region, scalePercent);
const unionBox = rectToPixels(
getRectUnion(region.rect, targetRect),
sourceWidth,
sourceHeight
);
const sourceBox = region.pixelRect;
const protectedPersonSide = getScaleAdjustProtectedPersonSide(region.prompt);
const leftPad = Math.max(10, Math.round(sourceBox.width * 0.09));
const rightPad =
protectedPersonSide === "right"
? Math.max(6, Math.round(sourceBox.width * 0.025))
: Math.max(10, Math.round(sourceBox.width * 0.09));
const topPad = Math.max(10, Math.round(sourceBox.height * 0.055));
const bottomPad = Math.max(10, Math.round(sourceBox.height * 0.035));
const repairLeft = clamp(
Math.min(unionBox.left, sourceBox.left) - leftPad,
0,
Math.max(0, sourceWidth - 1)
);
const repairTop = clamp(
Math.min(unionBox.top, sourceBox.top) - topPad,
0,
Math.max(0, sourceHeight - 1)
);
const repairRight = clamp(
Math.max(unionBox.left + unionBox.width, sourceBox.left + sourceBox.width) +
rightPad,
repairLeft + 1,
sourceWidth
);
const repairBottom = clamp(
Math.max(
unionBox.top + unionBox.height,
sourceBox.top + sourceBox.height
) + bottomPad,
repairTop + 1,
sourceHeight
);
repairRects.push(
`<rect x="${repairLeft}" y="${repairTop}" width="${Math.max(
1,
repairRight - repairLeft
)}" height="${Math.max(1, repairBottom - repairTop)}" rx="${Math.max(
8,
Math.round(blur * 0.9)
)}" ry="${Math.max(8, Math.round(blur * 0.9))}" fill="#ffffff" filter="url(#soft-repair)" />`
);
const workspaceBox = rectToPixels(
getScaleAdjustPatchWorkspaceRect([region], scalePercent),
sourceWidth,
sourceHeight
);
const panelBottom = clamp(
sourceBox.top + Math.round(sourceBox.height * 0.38),
repairTop + 1,
sourceHeight
);
repairRects.push(
`<rect x="${workspaceBox.left}" y="${repairTop}" width="${Math.max(
1,
workspaceBox.width
)}" height="${Math.max(1, panelBottom - repairTop)}" rx="${Math.max(
8,
Math.round(blur * 0.9)
)}" ry="${Math.max(8, Math.round(blur * 0.9))}" fill="#ffffff" filter="url(#soft-repair)" />`
);
if (protectedPersonSide === "right") {
const protectLeft = clamp(
sourceBox.left + Math.round(sourceBox.width * 0.73),
0,
Math.max(0, sourceWidth - 1)
);
const protectTop = clamp(
sourceBox.top + Math.round(sourceBox.height * 0.26),
0,
Math.max(0, sourceHeight - 1)
);
protectRects.push(
`<rect x="${protectLeft}" y="${protectTop}" width="${Math.max(
1,
sourceWidth - protectLeft
)}" height="${Math.max(1, sourceHeight - protectTop)}" fill="#000000" filter="url(#soft-protect)" />`
);
} else if (protectedPersonSide === "left") {
const protectRight = clamp(
sourceBox.left + Math.round(sourceBox.width * 0.27),
1,
sourceWidth
);
const protectTop = clamp(
sourceBox.top + Math.round(sourceBox.height * 0.26),
0,
Math.max(0, sourceHeight - 1)
);
protectRects.push(
`<rect x="0" y="${protectTop}" width="${protectRight}" height="${Math.max(
1,
sourceHeight - protectTop
)}" fill="#000000" filter="url(#soft-protect)" />`
);
}
}
const repairSvg = Buffer.from(
`<svg xmlns="http://www.w3.org/2000/svg" width="${sourceWidth}" height="${sourceHeight}" viewBox="0 0 ${sourceWidth} ${sourceHeight}">
<defs>
<filter id="soft-repair" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="${blur}" />
</filter>
<filter id="soft-protect" x="-8%" y="-8%" width="116%" height="116%">
<feGaussianBlur stdDeviation="${Math.max(4, Math.round(blur * 0.55))}" />
</filter>
</defs>
<rect x="0" y="0" width="${sourceWidth}" height="${sourceHeight}" fill="#000000" />
${repairRects.join("\n")}
${protectRects.join("\n")}
</svg>`
);
const repairAlpha = await sharp(repairSvg, { failOn: "none" })
.resize(sourceWidth, sourceHeight, { fit: "fill" })
.greyscale()
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(repairAlpha[index] || 0, productAlpha[index] || 0);
const value = Math.max(productAlpha[index] || 0, repairAlpha[index] || 0);
rgba[offset] = 255;
rgba[offset + 1] = 255;
rgba[offset + 2] = 255;
@ -3920,12 +3807,18 @@ 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 && compositeClipMaskBuffer
productLockComposites.length && sourceOnlyRepairMaskBuffer
? await buildScaleAdjustProductLockBackgroundRepairMaskBuffer({
productSourceMaskBuffer: compositeClipMaskBuffer,
regions,
scalePercent,
productSourceMaskBuffer: sourceOnlyRepairMaskBuffer,
sourceHeight,
sourceWidth,
})
@ -3961,6 +3854,7 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
async function createScaleAdjustPatchReference(params: {
fullFrameReferenceOnly?: boolean;
minimalProductLockMaskEditReferences?: boolean;
regions: NormalizedLocalEditRegion[];
scalePercent?: ScaleAdjustPercent | null;
sourceBuffer: Buffer;
@ -3999,6 +3893,60 @@ 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,
@ -5350,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,
@ -5528,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 =
@ -5563,14 +5530,19 @@ 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 &&
@ -5731,17 +5703,11 @@ export async function POST(request: NextRequest) {
: [];
const scaleAdjustProtectedRegions =
isScaleAdjustOperation && scaleAdjustPatchReference
? useScaleAdjustProductLockComposite && useScaleAdjustProductLockMaskEdit
? [
scaleAdjustPatchReference.workspaceRect,
...scaleAdjustProtectedChangeRegions,
]
: scaleAdjustProtectedChangeRegions
? scaleAdjustProtectedChangeRegions
: scaleAdjustCompositeRegions;
const scaleAdjustProductLockRepairClipMaskUrl =
useScaleAdjustProductLockComposite && useScaleAdjustProductLockMaskEdit
? scaleAdjustPatchReference?.productLockBackgroundRepairMaskUrl ||
scaleAdjustPatchReference?.productLockEditMaskUrl ||
scaleAdjustPatchReference?.compositeClipMaskUrl ||
undefined
: scaleAdjustPatchReference?.compositeClipMaskUrl || undefined;
@ -6025,18 +5991,19 @@ 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: scaleAdjustProductLockRepairClipMaskUrl,

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

@ -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]);