Compare commits
10 Commits
a201ebb346
...
7c1d985c09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c1d985c09 | ||
|
|
67b526408a | ||
|
|
f46c0b192d | ||
|
|
15fc9ebea9 | ||
|
|
961ce30500 | ||
|
|
0a66dbc0bc | ||
|
|
6b70865235 | ||
|
|
c60a3bb226 | ||
|
|
3828ec792d | ||
|
|
40ce24feb8 |
@ -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,
|
||||
|
||||
@ -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 变化时重新挂载
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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]"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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]);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user