feat(canvas): 人物质感调节 - 支持图片引用与五维质感生成参数
This commit is contained in:
@@ -502,6 +502,7 @@ function InfiniteCanvasPage() {
|
||||
emotionNodeId,
|
||||
annotationNodeId,
|
||||
createImageReversePromptNodes,
|
||||
createPortraitTextureNode,
|
||||
cropImageNode,
|
||||
cropNodeId,
|
||||
extractVideoLastFrame,
|
||||
@@ -1525,6 +1526,7 @@ function InfiniteCanvasPage() {
|
||||
onAnnotate={(node) => setAnnotationNodeId(node.id)}
|
||||
onMaskEdit={(node) => setMaskEditNodeId(node.id)}
|
||||
onEmotion={(node) => { setDialogNodeId(null); setEmotionNodeId((current) => current === node.id ? null : node.id); }}
|
||||
onPortraitTexture={createPortraitTextureNode}
|
||||
onCrop={(node) => setCropNodeId(node.id)}
|
||||
onSplit={(node) => setSplitNodeId(node.id)}
|
||||
onUpscale={(node) => setUpscaleNodeId(node.id)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { buildNodeGenerationContext, hydrateNodeGenerationContext } from "@/comp
|
||||
import type { CanvasNodeGenerationMode } from "@/components/canvas/canvas-node-prompt-panel";
|
||||
import { buildGenerationConfig, isGenerationCanceled, supportsVideoReferenceAudio } from "@/lib/canvas/canvas-project-generation";
|
||||
import { isGenerationTaskCapacityError } from "@/lib/canvas/canvas-generation-batch";
|
||||
import { buildPortraitTexturePrompt } from "@/lib/canvas/canvas-portrait-texture";
|
||||
import { expandSkillMentions } from "@/lib/canvas/canvas-skill-mentions";
|
||||
import { generationFailureMetadata } from "@/lib/generation-error";
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
@@ -80,6 +81,9 @@ export function useCanvasGenerationExecutor({
|
||||
const controller = startGenerationRequest(nodeId, nodeId, nodeId, options?.controller);
|
||||
const sourceTextContent = sourceNode?.type === CanvasNodeType.Text ? sourceNode.metadata?.content?.trim() || "" : "";
|
||||
const editingTextNode = mode === "text" && Boolean(sourceTextContent);
|
||||
const generationPrompt = mode === "image" && sourceNode?.metadata?.portraitTexture
|
||||
? buildPortraitTexturePrompt(prompt, sourceNode.metadata.portraitTexture)
|
||||
: prompt;
|
||||
const isPreparingEmptyImage = mode === "image" && sourceNode?.type === CanvasNodeType.Image && !sourceNode.metadata?.content;
|
||||
if (isPreparingEmptyImage) {
|
||||
setNodes((current) =>
|
||||
@@ -107,7 +111,7 @@ export function useCanvasGenerationExecutor({
|
||||
let rawGenerationContext: Awaited<ReturnType<typeof hydrateNodeGenerationContext>>;
|
||||
try {
|
||||
rawGenerationContext = await hydrateNodeGenerationContext(
|
||||
buildNodeGenerationContext(nodeId, nodesRef.current, connectionsRef.current, editingTextNode ? `请根据要求修改以下文本。\n\n原文:\n${sourceTextContent}\n\n修改要求:\n${prompt}` : prompt),
|
||||
buildNodeGenerationContext(nodeId, nodesRef.current, connectionsRef.current, editingTextNode ? `请根据要求修改以下文本。\n\n原文:\n${sourceTextContent}\n\n修改要求:\n${prompt}` : generationPrompt),
|
||||
projectId,
|
||||
domainProjectId,
|
||||
mode,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
supportsVideoReferenceAudio,
|
||||
} from "@/lib/canvas/canvas-project-generation";
|
||||
import { expandSkillMentions } from "@/lib/canvas/canvas-skill-mentions";
|
||||
import { buildPortraitTexturePrompt } from "@/lib/canvas/canvas-portrait-texture";
|
||||
import { generationFailureMetadata, unchangedModeratedPrompt } from "@/lib/generation-error";
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
import { storeGeneratedAudio } from "@/services/api/audio";
|
||||
@@ -77,13 +78,16 @@ export function useCanvasGenerationRetry({ projectId, domainProjectId, activated
|
||||
}
|
||||
|
||||
const retryPromptSource = sourceNode.metadata?.composerContent || sourceNode.metadata?.prompt || node.metadata?.prompt || "";
|
||||
const retryContextPrompt = retryMode === "image" && sourceNode.metadata?.portraitTexture
|
||||
? buildPortraitTexturePrompt(retryPromptSource, sourceNode.metadata.portraitTexture)
|
||||
: retryPromptSource;
|
||||
if (unchangedModeratedPrompt(node.metadata, retryPromptSource)) {
|
||||
message.warning("该提示词未通过内容审核,请先修改提示词再重新生成");
|
||||
return;
|
||||
}
|
||||
let rawContext: Awaited<ReturnType<typeof hydrateNodeGenerationContext>> | null;
|
||||
try {
|
||||
const baseContext = buildNodeGenerationContext(sourceNode.id, nodesRef.current, connectionsRef.current, retryPromptSource);
|
||||
const baseContext = buildNodeGenerationContext(sourceNode.id, nodesRef.current, connectionsRef.current, retryContextPrompt);
|
||||
rawContext = hasSavedImageMetadata && !baseContext.characterReferences.length ? null : await hydrateNodeGenerationContext(baseContext, projectId, domainProjectId, retryMode, retryMode === "video" && supportsVideoReferenceAudio(generationConfig));
|
||||
} catch (error) {
|
||||
const failure = generationFailureMetadata(error, retryPromptSource);
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from "@/lib/canvas/canvas-project-generation";
|
||||
import { fitNodeSize } 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 { captureVideoLastFrame } from "@/lib/canvas/canvas-video-frame";
|
||||
import { mergeVideos, type MergeVideoProgress } from "@/lib/canvas/canvas-video-merge";
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
@@ -129,6 +130,42 @@ export function useCanvasMediaTools({
|
||||
setContextMenu(null);
|
||||
}, [effectiveConfig.model, effectiveConfig.textModel, message, setConnections, setContextMenu, setDialogNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
|
||||
|
||||
const createPortraitTextureNode = useCallback((node: CanvasNodeData) => {
|
||||
if (node.type !== CanvasNodeType.Image || !node.metadata?.content) {
|
||||
message.warning("图片节点为空,无法调节人物质感");
|
||||
return;
|
||||
}
|
||||
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);
|
||||
setHoveredNodeId(null);
|
||||
setToolbarNodeId(null);
|
||||
}, [effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.quality, effectiveConfig.size, effectiveConfig.transparentBackground, message, setConnections, setContextMenu, setDialogNodeId, setHoveredNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds, setToolbarNodeId]);
|
||||
|
||||
const cropImageNode = useCallback(async (node: CanvasNodeData, crop: CanvasImageCropRect) => {
|
||||
if (!node.metadata?.content) return;
|
||||
const cropped = await cropDataUrl(node.metadata.content, crop);
|
||||
@@ -431,6 +468,7 @@ export function useCanvasMediaTools({
|
||||
emotionNodeId,
|
||||
annotationNodeId,
|
||||
createImageReversePromptNodes,
|
||||
createPortraitTextureNode,
|
||||
cropImageNode,
|
||||
cropNodeId,
|
||||
extractVideoLastFrame,
|
||||
|
||||
Reference in New Issue
Block a user