|
|
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
|
|
} from "@/lib/canvas/canvas-project-generation";
|
|
|
|
|
import { fitNodeSize, VIDEO_NODE_MAX_SIZE } from "@/lib/canvas/canvas-node-size";
|
|
|
|
|
import { compositeEmotionImage, emotionGenerationSize } from "@/lib/canvas/canvas-emotion";
|
|
|
|
|
import { DEFAULT_PORTRAIT_TEXTURE_SETTINGS } from "@/lib/canvas/canvas-portrait-texture";
|
|
|
|
|
import { DEFAULT_PORTRAIT_TEXTURE_SETTINGS, buildPortraitTexturePrompt } from "@/lib/canvas/canvas-portrait-texture";
|
|
|
|
|
import { captureVideoLastFrame } from "@/lib/canvas/canvas-video-frame";
|
|
|
|
|
import { mergeVideos, type MergeVideoProgress } from "@/lib/canvas/canvas-video-merge";
|
|
|
|
|
import { generationErrorMessage } from "@/lib/generation-error";
|
|
|
|
|
@@ -129,41 +129,50 @@ export function useCanvasMediaTools({
|
|
|
|
|
setContextMenu(null);
|
|
|
|
|
}, [effectiveConfig.model, effectiveConfig.textModel, message, setConnections, setContextMenu, setDialogNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
|
|
|
|
|
|
|
|
|
|
const createPortraitTextureNode = useCallback((node: CanvasNodeData) => {
|
|
|
|
|
const generatePortraitTextureNode = useCallback(async (node: CanvasNodeData) => {
|
|
|
|
|
if (node.type !== CanvasNodeType.Image || !node.metadata?.content) {
|
|
|
|
|
message.warning("图片节点为空,无法调节人物质感");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const generationConfig = { ...buildGenerationConfig(effectiveConfig, node, "image"), count: "1" };
|
|
|
|
|
if (!isAiConfigReady(generationConfig, generationConfig.model)) {
|
|
|
|
|
navigateToSettings({ continueCreation: true });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const childId = nanoid();
|
|
|
|
|
const imageSpec = NODE_DEFAULT_SIZE[CanvasNodeType.Image];
|
|
|
|
|
const composerContent = `@[node:${node.id}]`;
|
|
|
|
|
const child = createCanvasNode(
|
|
|
|
|
CanvasNodeType.Image,
|
|
|
|
|
{
|
|
|
|
|
x: node.position.x + node.width + 96 + imageSpec.width / 2,
|
|
|
|
|
y: node.position.y + node.height / 2,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
generationMode: "image",
|
|
|
|
|
model: effectiveConfig.imageModel || effectiveConfig.model,
|
|
|
|
|
size: node.metadata.size || effectiveConfig.size,
|
|
|
|
|
quality: node.metadata.quality || effectiveConfig.quality,
|
|
|
|
|
transparentBackground: node.metadata.transparentBackground || effectiveConfig.transparentBackground,
|
|
|
|
|
count: 1,
|
|
|
|
|
prompt: composerContent,
|
|
|
|
|
composerContent,
|
|
|
|
|
portraitTexture: { ...DEFAULT_PORTRAIT_TEXTURE_SETTINGS },
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
child.title = "人物质感调节";
|
|
|
|
|
setNodes((current) => [...current, child]);
|
|
|
|
|
setConnections((current) => [...current, { id: nanoid(), fromNodeId: node.id, toNodeId: child.id }]);
|
|
|
|
|
setSelectedNodeIds(new Set([child.id]));
|
|
|
|
|
setSelectedConnectionId(null);
|
|
|
|
|
setDialogNodeId(child.id);
|
|
|
|
|
setContextMenu(null);
|
|
|
|
|
const source = nodeReferenceImage(node);
|
|
|
|
|
if (!source) return;
|
|
|
|
|
const prompt = buildPortraitTexturePrompt(composerContent, { ...DEFAULT_PORTRAIT_TEXTURE_SETTINGS, ...node.metadata?.portraitTexture });
|
|
|
|
|
const generationMetadata = buildImageGenerationMetadata("edit", generationConfig, 1, [source]);
|
|
|
|
|
const portraitTextureSettings = { ...DEFAULT_PORTRAIT_TEXTURE_SETTINGS, ...node.metadata?.portraitTexture };
|
|
|
|
|
setHoveredNodeId(null);
|
|
|
|
|
setToolbarNodeId(null);
|
|
|
|
|
}, [effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.quality, effectiveConfig.size, effectiveConfig.transparentBackground, message, setConnections, setContextMenu, setDialogNodeId, setHoveredNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds, setToolbarNodeId]);
|
|
|
|
|
setRunningNodeId(childId);
|
|
|
|
|
setNodes((current) => [...current, { id: childId, type: CanvasNodeType.Image, title: "人物质感调节", position: { x: node.position.x + node.width + 96 + imageSpec.width / 2, y: node.position.y + node.height / 2 }, width: imageSpec.width, height: imageSpec.height, metadata: { prompt, status: NODE_STATUS_LOADING, composerContent, portraitTexture: portraitTextureSettings, ...generationMetadata } }]);
|
|
|
|
|
setConnections((current) => [...current, { id: nanoid(), fromNodeId: node.id, toNodeId: childId }]);
|
|
|
|
|
setSelectedNodeIds(new Set([childId]));
|
|
|
|
|
setSelectedConnectionId(null);
|
|
|
|
|
setDialogNodeId(childId);
|
|
|
|
|
const controller = startGenerationRequest(childId, node.id, childId);
|
|
|
|
|
try {
|
|
|
|
|
const result = await runBackendCanvasGenerationTask({ projectId, nodeId: childId, mode: "image", prompt, config: generationConfig, referenceImages: [source], signal: controller.signal, metadata: { sourceNodeId: node.id, edit: "portraitTexture", portraitTexture: portraitTextureSettings }, onTaskCreated: (task) => bindGenerationTask(childId, task) });
|
|
|
|
|
const image = result.images?.[0];
|
|
|
|
|
if (!image?.dataUrl) throw new Error("后端任务没有返回图片");
|
|
|
|
|
const uploaded = await uploadImage(image.dataUrl);
|
|
|
|
|
const size = fitNodeSize(uploaded.width, uploaded.height, imageSpec.width, imageSpec.height);
|
|
|
|
|
setNodes((current) => current.map((item) => item.id === childId ? { ...item, width: size.width, height: size.height, metadata: { ...item.metadata, ...imageMetadata(uploaded), prompt, ...generationMetadata, portraitTexture: portraitTextureSettings } } : item));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (isGenerationCanceled(error)) return;
|
|
|
|
|
const details = generationErrorMessage(error);
|
|
|
|
|
message.error(details);
|
|
|
|
|
setNodes((current) => current.map((item) => item.id === childId ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, errorDetails: details } } : item));
|
|
|
|
|
} finally {
|
|
|
|
|
finishGenerationRequest(childId, controller);
|
|
|
|
|
setRunningNodeId(null);
|
|
|
|
|
}
|
|
|
|
|
}, [bindGenerationTask, effectiveConfig, finishGenerationRequest, isAiConfigReady, message, projectId, setConnections, setDialogNodeId, setNodes, setRunningNodeId, setSelectedConnectionId, setSelectedNodeIds, startGenerationRequest]);
|
|
|
|
|
|
|
|
|
|
const cropImageNode = useCallback(async (node: CanvasNodeData, crop: CanvasImageCropRect) => {
|
|
|
|
|
if (!node.metadata?.content) return;
|
|
|
|
|
@@ -467,7 +476,7 @@ export function useCanvasMediaTools({
|
|
|
|
|
emotionNodeId,
|
|
|
|
|
annotationNodeId,
|
|
|
|
|
createImageReversePromptNodes,
|
|
|
|
|
createPortraitTextureNode,
|
|
|
|
|
generatePortraitTextureNode,
|
|
|
|
|
cropImageNode,
|
|
|
|
|
cropNodeId,
|
|
|
|
|
extractVideoLastFrame,
|
|
|
|
|
|