feat: add split-tile upscale toolbar
This commit is contained in:
parent
d622b18ddf
commit
7f796a5bfd
@ -684,6 +684,7 @@ export async function POST(request: NextRequest) {
|
||||
let chatId: string | null = null; // 聊天ID(可选)
|
||||
let messageId: string | null = null; // 消息ID(可选)
|
||||
let sourceImageUrl: string | null = null;
|
||||
let requestedScaleFactor: number | undefined;
|
||||
|
||||
if (contentType.includes("application/json")) {
|
||||
console.log("[Enhance API] 解析JSON请求体...");
|
||||
@ -691,6 +692,11 @@ export async function POST(request: NextRequest) {
|
||||
prompt = body.prompt || "图片变清晰处理";
|
||||
chatId = body.chatId || null;
|
||||
messageId = body.messageId || null;
|
||||
const parsedScaleFactor = Number.parseInt(String(body.scaleFactor || ""), 10);
|
||||
requestedScaleFactor =
|
||||
Number.isFinite(parsedScaleFactor) && [1, 2, 4, 8].includes(parsedScaleFactor)
|
||||
? parsedScaleFactor
|
||||
: undefined;
|
||||
|
||||
// 优先使用 imageUrl,其次使用 image_base64
|
||||
if (body.imageUrl) {
|
||||
@ -767,7 +773,7 @@ export async function POST(request: NextRequest) {
|
||||
const result = await processWithRealESRGAN(sourceImageUrl, {
|
||||
backupApiUrl: config.backupApiUrl!,
|
||||
backupApiKey: config.backupApiKey!,
|
||||
backupScale: config.backupScale || "4",
|
||||
backupScale: String(requestedScaleFactor || Number.parseInt(config.backupScale || "4", 10) || 4),
|
||||
backupModel: config.backupModel || "RealESRGAN_x4plus",
|
||||
});
|
||||
|
||||
@ -806,7 +812,7 @@ export async function POST(request: NextRequest) {
|
||||
messageId: messageId || null,
|
||||
thumbnailUrl,
|
||||
originalUrl,
|
||||
imageSize: `${config.backupScale}x放大`,
|
||||
imageSize: `${requestedScaleFactor || Number.parseInt(config.backupScale || "4", 10) || 4}x放大`,
|
||||
prompt: prompt || "Real-ESRGAN图片变清晰处理",
|
||||
modelId: `realesrgan-${config.backupModel}`,
|
||||
}).returning());
|
||||
@ -851,7 +857,7 @@ export async function POST(request: NextRequest) {
|
||||
sourceRowId: savedImage.id,
|
||||
primaryParentAssetId: sourceAssets[0]?.id ?? null,
|
||||
metadata: {
|
||||
imageSize: `${config.backupScale}x放大`,
|
||||
imageSize: `${requestedScaleFactor || Number.parseInt(config.backupScale || "4", 10) || 4}x放大`,
|
||||
apiType: "realesrgan-url",
|
||||
},
|
||||
});
|
||||
@ -926,7 +932,13 @@ export async function POST(request: NextRequest) {
|
||||
// 使用异步模式(sync=0),根据官方示例
|
||||
formData.append("sync", "0");
|
||||
formData.append("type", config.type || "clean"); // 默认通用清晰化
|
||||
const scaleFactorValue = typeof config.scaleFactor === "number" ? config.scaleFactor : (config.scaleFactor === "auto" ? undefined : Number.parseInt(String(config.scaleFactor)));
|
||||
const scaleFactorValue =
|
||||
requestedScaleFactor ||
|
||||
(typeof config.scaleFactor === "number"
|
||||
? config.scaleFactor
|
||||
: config.scaleFactor === "auto"
|
||||
? undefined
|
||||
: Number.parseInt(String(config.scaleFactor)));
|
||||
if (scaleFactorValue) {
|
||||
formData.append("scale_factor", String(scaleFactorValue));
|
||||
}
|
||||
|
||||
@ -1226,6 +1226,12 @@ function getNodeGridSplitSourceUrl(node: CloneNode | null | undefined) {
|
||||
return getNodeMediaUrl(node) || getNodeSourceMediaUrl(node);
|
||||
}
|
||||
|
||||
function getGridSplitCellLabel(index: number, split: Pick<GridSplitState, "columns">) {
|
||||
const row = Math.floor(index / split.columns) + 1;
|
||||
const column = (index % split.columns) + 1;
|
||||
return `${row}-${column}`;
|
||||
}
|
||||
|
||||
function getOfficialImagePresetFunctionKey(value?: unknown) {
|
||||
const text = String(value || "").trim();
|
||||
if (!text) return null;
|
||||
@ -3555,6 +3561,282 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
[]
|
||||
);
|
||||
|
||||
const createGridSplitImageNodes = useCallback(
|
||||
async (nodeId: string) => {
|
||||
const anchor = nodes.find((node) => node.id === nodeId);
|
||||
const split = getNodeGridSplitState(anchor);
|
||||
const sourceUrl = getNodeGridSplitSourceUrl(anchor);
|
||||
if (!anchor || !split || split.selectedIndices.length === 0 || !sourceUrl) {
|
||||
toast.error("请先选择需要处理的宫格");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIndices = split.selectedIndices.slice();
|
||||
const selectedAssets = await Promise.all(
|
||||
selectedIndices.map(async (index) => {
|
||||
const rect = split.rects[index];
|
||||
if (!rect) return null;
|
||||
const url = await createGridSplitReferenceAsset({
|
||||
index,
|
||||
nodeId: anchor.id,
|
||||
rect,
|
||||
sourceUrl,
|
||||
}).catch(() => "");
|
||||
if (!url) return null;
|
||||
return { index, rect, url };
|
||||
})
|
||||
);
|
||||
const validAssets = selectedAssets.filter((item): item is NonNullable<typeof item> => Boolean(item));
|
||||
if (validAssets.length === 0) {
|
||||
toast.error("所选宫格暂时无法切出独立图片");
|
||||
return;
|
||||
}
|
||||
|
||||
const startX = Math.round(anchor.x + anchor.width + OFFICIAL_NODE_GAP);
|
||||
const columns = validAssets.length >= 4 ? 2 : 1;
|
||||
const columnGap = 52;
|
||||
const rowGap = 34;
|
||||
const tileWidth = 428;
|
||||
const tileHeight = 240;
|
||||
const totalHeight =
|
||||
Math.ceil(validAssets.length / columns) * tileHeight +
|
||||
Math.max(0, Math.ceil(validAssets.length / columns) - 1) * rowGap;
|
||||
const startY = Math.round(anchor.y + anchor.height / 2 - totalHeight / 2);
|
||||
|
||||
const createdNodes = validAssets.map((asset, order) => {
|
||||
const column = order % columns;
|
||||
const row = Math.floor(order / columns);
|
||||
const title = `镜头 ${getGridSplitCellLabel(asset.index, split)}`;
|
||||
return createMediaResourceNode({
|
||||
action: "IMAGE_RESOURCE",
|
||||
height: tileHeight,
|
||||
mediaType: "image",
|
||||
mediaUrl: asset.url,
|
||||
title,
|
||||
width: tileWidth,
|
||||
x: startX + column * (tileWidth + columnGap),
|
||||
y: startY + row * (tileHeight + rowGap),
|
||||
});
|
||||
}).map((node, order) => {
|
||||
const asset = validAssets[order];
|
||||
return {
|
||||
...node,
|
||||
subtitle: "宫格镜头",
|
||||
draftValues: {
|
||||
...(node.draftValues || {}),
|
||||
cropTile: {
|
||||
columns: split.columns,
|
||||
detectedDivider: split.detectedDivider,
|
||||
index: asset.index,
|
||||
rect: asset.rect,
|
||||
rows: split.rows,
|
||||
sourceCompositeNodeId: anchor.id,
|
||||
sourceMode: split.sourceMode,
|
||||
},
|
||||
mediaUrl: asset.url,
|
||||
outputSizeLabel: undefined,
|
||||
params: {
|
||||
...(node.draftValues?.params as Record<string, unknown> | undefined),
|
||||
cropRect: asset.rect,
|
||||
sourceNodeId: anchor.id,
|
||||
sourceUrl: asset.url,
|
||||
},
|
||||
sourceCompositeNodeId: anchor.id,
|
||||
sourceNodeId: anchor.id,
|
||||
sourceUrl: asset.url,
|
||||
url: [asset.url],
|
||||
},
|
||||
} satisfies CloneNode;
|
||||
});
|
||||
|
||||
setNodes((current) => [...current, ...createdNodes]);
|
||||
setEdges((current) => [
|
||||
...current,
|
||||
...createdNodes.map((node) => ({
|
||||
id: makeId("edge"),
|
||||
source: anchor.id,
|
||||
target: node.id,
|
||||
})),
|
||||
]);
|
||||
setSelectedNodeId(createdNodes[0]?.id || anchor.id);
|
||||
setCanvasCommand(null);
|
||||
setNodeContextMenu(null);
|
||||
setConnectionMenu(null);
|
||||
setActivePanel(null);
|
||||
toast.success(`已创建 ${createdNodes.length} 个宫格图片节点`);
|
||||
},
|
||||
[nodes]
|
||||
);
|
||||
|
||||
const upscaleSelectedGridSplit = useCallback(
|
||||
async (nodeId: string, scaleFactor: number) => {
|
||||
const anchor = nodes.find((node) => node.id === nodeId);
|
||||
const split = getNodeGridSplitState(anchor);
|
||||
const sourceUrl = getNodeGridSplitSourceUrl(anchor);
|
||||
if (!anchor || !split || split.selectedIndices.length === 0 || !sourceUrl) {
|
||||
toast.error("请先选择需要高清放大的宫格");
|
||||
return;
|
||||
}
|
||||
|
||||
const definition = getConfiguredFunction(videoCanvasFunctionConfig, "hd-upscale");
|
||||
const selectedIndices = split.selectedIndices.slice();
|
||||
const selectedAssets = await Promise.all(
|
||||
selectedIndices.map(async (index) => {
|
||||
const rect = split.rects[index];
|
||||
if (!rect) return null;
|
||||
const url = await createGridSplitReferenceAsset({
|
||||
index,
|
||||
nodeId: anchor.id,
|
||||
rect,
|
||||
sourceUrl,
|
||||
}).catch(() => "");
|
||||
if (!url) return null;
|
||||
return { index, rect, url };
|
||||
})
|
||||
);
|
||||
const validAssets = selectedAssets.filter((item): item is NonNullable<typeof item> => Boolean(item));
|
||||
if (validAssets.length === 0) {
|
||||
toast.error("所选宫格暂时无法执行高清放大");
|
||||
return;
|
||||
}
|
||||
|
||||
const startX = Math.round(anchor.x + anchor.width + OFFICIAL_NODE_GAP);
|
||||
const columns = validAssets.length >= 4 ? 2 : 1;
|
||||
const columnGap = 52;
|
||||
const rowGap = 34;
|
||||
const tileWidth = 560;
|
||||
const tileHeight = 315;
|
||||
const totalHeight =
|
||||
Math.ceil(validAssets.length / columns) * tileHeight +
|
||||
Math.max(0, Math.ceil(validAssets.length / columns) - 1) * rowGap;
|
||||
const startY = Math.round(anchor.y + anchor.height / 2 - totalHeight / 2);
|
||||
|
||||
const pendingNodes = validAssets.map((asset, order) => {
|
||||
const column = order % columns;
|
||||
const row = Math.floor(order / columns);
|
||||
const cellLabel = getGridSplitCellLabel(asset.index, split);
|
||||
const pending = createMediaResourceNode({
|
||||
action: "IMAGE_RESOURCE",
|
||||
height: tileHeight,
|
||||
mediaType: "image",
|
||||
mediaUrl: asset.url,
|
||||
prompt: definition.prompt,
|
||||
title: `宫格高清 ${cellLabel}`,
|
||||
width: tileWidth,
|
||||
x: startX + column * (tileWidth + columnGap),
|
||||
y: startY + row * (tileHeight + rowGap),
|
||||
});
|
||||
return {
|
||||
...pending,
|
||||
subtitle: "生成中",
|
||||
draftValues: {
|
||||
...(pending.draftValues || {}),
|
||||
action: "IMAGE_RESOURCE",
|
||||
functionKey: definition.key,
|
||||
generated: false,
|
||||
generationProgress: 2,
|
||||
generationStatus: "pending",
|
||||
mediaType: "image",
|
||||
mediaUrl: asset.url,
|
||||
nodeType: "image",
|
||||
outputSizeLabel: `${scaleFactor}x 高清`,
|
||||
params: {
|
||||
functionKey: definition.key,
|
||||
mode: "grid-split-upscale",
|
||||
prompt: definition.prompt,
|
||||
scaleFactor,
|
||||
sourceNodeId: anchor.id,
|
||||
sourceTileIndex: asset.index,
|
||||
sourceUrl: asset.url,
|
||||
},
|
||||
pendingLabel: "生成中",
|
||||
prompt: definition.prompt,
|
||||
sourceCompositeNodeId: anchor.id,
|
||||
sourceNodeId: anchor.id,
|
||||
sourceUrl: asset.url,
|
||||
toolbarOption: `x${scaleFactor}`,
|
||||
url: [asset.url],
|
||||
},
|
||||
} satisfies CloneNode;
|
||||
});
|
||||
|
||||
setNodes((current) => [...current, ...pendingNodes]);
|
||||
setEdges((current) => [
|
||||
...current,
|
||||
...pendingNodes.map((node) => ({
|
||||
id: makeId("edge"),
|
||||
source: anchor.id,
|
||||
target: node.id,
|
||||
})),
|
||||
]);
|
||||
setSelectedNodeId(anchor.id);
|
||||
setCanvasCommand(null);
|
||||
setNodeContextMenu(null);
|
||||
setConnectionMenu(null);
|
||||
setActivePanel(null);
|
||||
|
||||
for (const [order, asset] of validAssets.entries()) {
|
||||
const pendingNode = pendingNodes[order];
|
||||
const response = await imageGenerationService.enhanceImage(asset.url, { scaleFactor });
|
||||
const generatedUrl = response.success ? response.data?.url || "" : "";
|
||||
if (generatedUrl) {
|
||||
setNodes((current) =>
|
||||
current.map((node) =>
|
||||
node.id === pendingNode.id
|
||||
? {
|
||||
...node,
|
||||
subtitle: "生成结果",
|
||||
draftValues: {
|
||||
...clearGridSplitDraftValues(node.draftValues),
|
||||
functionKey: definition.key,
|
||||
generated: true,
|
||||
generationProgress: 100,
|
||||
generationStatus: "complete",
|
||||
mediaType: "image",
|
||||
mediaUrl: generatedUrl,
|
||||
nodeType: "image",
|
||||
outputSizeLabel: `${scaleFactor}x 高清`,
|
||||
params: {
|
||||
...getNodeParams(node),
|
||||
functionKey: definition.key,
|
||||
prompt: definition.prompt,
|
||||
scaleFactor,
|
||||
sourceNodeId: anchor.id,
|
||||
sourceTileIndex: asset.index,
|
||||
sourceUrl: asset.url,
|
||||
},
|
||||
sourceCompositeNodeId: anchor.id,
|
||||
sourceNodeId: anchor.id,
|
||||
sourceUrl: asset.url,
|
||||
url: [generatedUrl],
|
||||
},
|
||||
}
|
||||
: node
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setNodes((current) =>
|
||||
current.map((node) =>
|
||||
node.id === pendingNode.id
|
||||
? {
|
||||
...node,
|
||||
draftValues: {
|
||||
...clearGridSplitDraftValues(node.draftValues),
|
||||
generationStatus: "failed",
|
||||
generationError: response.error || "宫格高清放大失败",
|
||||
},
|
||||
}
|
||||
: node
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toast.success(`已提交 ${validAssets.length} 张宫格图的 ${scaleFactor}x 高清放大`);
|
||||
},
|
||||
[nodes, videoCanvasFunctionConfig]
|
||||
);
|
||||
|
||||
const submitSelectedImagePrompt = useCallback(
|
||||
async (nodeId: string, payload: ComposerSubmitPayload) => {
|
||||
const anchor = nodes.find((node) => node.id === nodeId);
|
||||
@ -4480,6 +4762,9 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
functionConfig={videoCanvasFunctionConfig}
|
||||
modelOptions={canvasModelOptions}
|
||||
nodes={nodes}
|
||||
onClearGridSplitSelection={() => clearNodeGridSplitSelection(selectedPresetControlNode.id)}
|
||||
onGridSplitCreateNodes={() => createGridSplitImageNodes(selectedPresetControlNode.id)}
|
||||
onGridSplitUpscale={(scaleFactor) => upscaleSelectedGridSplit(selectedPresetControlNode.id, scaleFactor)}
|
||||
onComposerSubmit={(payload) => submitSelectedImagePrompt(selectedPresetControlNode.id, payload)}
|
||||
onImageToolAction={(action, option) => runSelectedImageToolAction(selectedPresetControlNode.id, action, option)}
|
||||
onPresetSubmit={(prompt, settings) =>
|
||||
@ -5225,12 +5510,124 @@ function MagnetGlyph() {
|
||||
);
|
||||
}
|
||||
|
||||
function GridSplitActionToolbar({
|
||||
anchor,
|
||||
creditCost,
|
||||
onClearSelection,
|
||||
onCreateNodes,
|
||||
onUpscale,
|
||||
selectedCount,
|
||||
}: {
|
||||
anchor?: PresetControlAnchor;
|
||||
creditCost: number;
|
||||
onClearSelection: () => void;
|
||||
onCreateNodes: () => void;
|
||||
onUpscale: (scaleFactor: number) => void;
|
||||
selectedCount: number;
|
||||
}) {
|
||||
const [scaleFactor, setScaleFactor] = useState(2);
|
||||
const [scaleMenuOpen, setScaleMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="pointer-events-auto absolute flex h-[58px] items-center gap-4 rounded-[14px] border border-white/8 bg-[#262626]/96 px-5 text-[16px] text-white/82 shadow-[0_20px_64px_rgba(0,0,0,0.42)] backdrop-blur-[10px]"
|
||||
style={
|
||||
anchor
|
||||
? {
|
||||
left: anchor.centerX,
|
||||
top: anchor.topY - (24 * anchor.zoom + 10),
|
||||
transform: `translateX(-50%) translateY(-100%) scale(${OFFICIAL_IMAGE_TOOLBAR_SCALE})`,
|
||||
transformOrigin: "bottom center",
|
||||
}
|
||||
: {
|
||||
left: "50%",
|
||||
top: 168,
|
||||
transform: `translateX(-50%) scale(${OFFICIAL_IMAGE_TOOLBAR_SCALE})`,
|
||||
transformOrigin: "top center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="flex h-10 w-10 items-center justify-center rounded-[10px] text-[22px] text-white/74 transition hover:bg-white/8 hover:text-white"
|
||||
onClick={onClearSelection}
|
||||
type="button"
|
||||
>
|
||||
↩
|
||||
</button>
|
||||
<div className="h-9 w-px bg-white/8" />
|
||||
<span className="flex items-center gap-2 text-white/74">
|
||||
<GridTilesGlyph />
|
||||
<span className="text-[15px]">已选 {selectedCount} 个宫格</span>
|
||||
</span>
|
||||
<div className="h-9 w-px bg-white/8" />
|
||||
<button
|
||||
className="flex h-10 items-center gap-2 rounded-[10px] px-2 text-white/84 transition hover:bg-white/8"
|
||||
onClick={onCreateNodes}
|
||||
type="button"
|
||||
>
|
||||
<WorkflowGlyph />
|
||||
<span className="whitespace-nowrap">创建生图节点</span>
|
||||
</button>
|
||||
<div className="h-9 w-px bg-white/8" />
|
||||
<button
|
||||
className="flex h-10 items-center gap-2 rounded-[10px] px-2 text-white/84 transition hover:bg-white/8"
|
||||
onClick={() => onUpscale(scaleFactor)}
|
||||
type="button"
|
||||
>
|
||||
<SceneToolIcon kind="hd" />
|
||||
<span className="whitespace-nowrap">生成高清图片</span>
|
||||
<span className="rounded-[7px] bg-white/10 px-2 py-0.5 text-[13px] text-white/72">
|
||||
↑{Math.max(0, creditCost)}
|
||||
</span>
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button
|
||||
className={cn(
|
||||
"flex h-10 min-w-[72px] items-center justify-between gap-2 rounded-[10px] px-3 text-white/84 transition hover:bg-white/8",
|
||||
scaleMenuOpen && "bg-white/8"
|
||||
)}
|
||||
onClick={() => setScaleMenuOpen((current) => !current)}
|
||||
type="button"
|
||||
>
|
||||
<span>{scaleFactor}倍</span>
|
||||
<span className="text-[14px] text-white/58">⌄</span>
|
||||
</button>
|
||||
{scaleMenuOpen ? (
|
||||
<div className="absolute right-0 top-[calc(100%+8px)] w-[108px] rounded-[12px] border border-white/8 bg-[#262626]/96 p-2 shadow-[0_18px_48px_rgba(0,0,0,0.42)] backdrop-blur-[12px]">
|
||||
{[2, 4].map((option) => (
|
||||
<button
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-center rounded-[8px] text-[14px] text-white/78 transition hover:bg-white/8 hover:text-white",
|
||||
scaleFactor === option && "bg-white/10 text-white"
|
||||
)}
|
||||
key={option}
|
||||
onClick={() => {
|
||||
setScaleFactor(option);
|
||||
setScaleMenuOpen(false);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{option}倍
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PresetWorkflowControls({
|
||||
anchor,
|
||||
functionConfig,
|
||||
mode,
|
||||
modelOptions,
|
||||
nodes,
|
||||
onClearGridSplitSelection,
|
||||
onGridSplitCreateNodes,
|
||||
onGridSplitUpscale,
|
||||
onComposerSubmit,
|
||||
onImageToolAction,
|
||||
onPresetSubmit,
|
||||
@ -5241,6 +5638,9 @@ function PresetWorkflowControls({
|
||||
mode: Exclude<ActivePresetControlMode, null>;
|
||||
modelOptions: CanvasModelOption[];
|
||||
nodes: CloneNode[];
|
||||
onClearGridSplitSelection: () => void;
|
||||
onGridSplitCreateNodes: () => void;
|
||||
onGridSplitUpscale: (scaleFactor: number) => void;
|
||||
onComposerSubmit: (payload: ComposerSubmitPayload) => void;
|
||||
onImageToolAction: (action: ImageToolbarAction, option?: string) => void;
|
||||
onPresetSubmit: (prompt: string, settings?: ComposerSettings) => void;
|
||||
@ -5248,6 +5648,21 @@ function PresetWorkflowControls({
|
||||
}) {
|
||||
if (isOfficialImagePresetFunctionKey(mode)) {
|
||||
const definition = getConfiguredFunction(functionConfig, mode);
|
||||
const gridSplit = getNodeGridSplitState(selectedNode);
|
||||
if (gridSplit && gridSplit.selectedIndices.length > 0) {
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
||||
<GridSplitActionToolbar
|
||||
anchor={anchor}
|
||||
creditCost={(getConfiguredFunction(functionConfig, "hd-upscale").creditCost || 0) * gridSplit.selectedIndices.length}
|
||||
onClearSelection={onClearGridSplitSelection}
|
||||
onCreateNodes={onGridSplitCreateNodes}
|
||||
onUpscale={onGridSplitUpscale}
|
||||
selectedCount={gridSplit.selectedIndices.length}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
||||
<CharacterPresetToolbar anchor={anchor} onToolAction={onImageToolAction} />
|
||||
@ -8180,6 +8595,7 @@ function PresetMediaCanvasNode({
|
||||
<GenerationPendingPreview
|
||||
errorMessage={getDraftString(node, "generationError")}
|
||||
onCancel={onCancelGeneration}
|
||||
previewUrl={mediaUrl}
|
||||
progress={getDraftNumber(node, "generationProgress", 2)}
|
||||
status={generationStatus}
|
||||
/>
|
||||
@ -8242,11 +8658,13 @@ function PresetMediaCanvasNode({
|
||||
function GenerationPendingPreview({
|
||||
errorMessage,
|
||||
onCancel,
|
||||
previewUrl,
|
||||
progress,
|
||||
status,
|
||||
}: {
|
||||
errorMessage?: string;
|
||||
onCancel: () => void;
|
||||
previewUrl?: string;
|
||||
progress: number;
|
||||
status: string;
|
||||
}) {
|
||||
@ -8255,6 +8673,15 @@ function GenerationPendingPreview({
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 flex items-center justify-center overflow-hidden bg-[#303030]">
|
||||
{previewUrl ? (
|
||||
<img
|
||||
alt=""
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-40 blur-[16px] saturate-[0.9]"
|
||||
draggable={false}
|
||||
onDragStart={preventNativeImageDrag}
|
||||
src={previewUrl}
|
||||
/>
|
||||
) : null}
|
||||
<style>{`
|
||||
@keyframes libtv-loading-sweep {
|
||||
0% { transform: translateX(0); opacity: 0; }
|
||||
|
||||
@ -355,12 +355,20 @@ export const imageGenerationService = {
|
||||
},
|
||||
|
||||
// 图片增强 - 使用图片URL
|
||||
async enhanceImage(imageUrl: string): Promise<ApiResponse<{ url: string }>> {
|
||||
async enhanceImage(
|
||||
imageUrl: string,
|
||||
options?: {
|
||||
scaleFactor?: number;
|
||||
}
|
||||
): Promise<ApiResponse<{ url: string }>> {
|
||||
try {
|
||||
const response = await fetch('/api/enhance', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ imageUrl }), // 使用 imageUrl 参数
|
||||
body: JSON.stringify({
|
||||
imageUrl,
|
||||
scaleFactor: options?.scaleFactor,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success && data.original_url) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user