From 8f20240df848a477086e4ec6c1b6daa5e2139a89 Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Sat, 8 Aug 2026 08:38:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20=E8=A7=86=E9=A2=91=E7=94=9F?= =?UTF-8?q?=E6=88=90=20-=20=E4=BB=85=E4=BD=BF=E7=94=A8=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E6=A1=86=E6=8F=90=E7=A4=BA=E8=AF=8D=E5=B9=B6=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E5=8C=96=E5=AA=92=E4=BD=93=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/service/provider.go | 11 ++++++----- .../components/canvas/canvas-node-generation.ts | 14 +++++++------- .../pages/canvas/use-canvas-generation-executor.ts | 9 ++++++--- .../pages/canvas/use-canvas-generation-retry.ts | 11 ++++++----- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/backend/internal/service/provider.go b/backend/internal/service/provider.go index bedce2162..a2e54c031 100644 --- a/backend/internal/service/provider.go +++ b/backend/internal/service/provider.go @@ -162,8 +162,12 @@ func (s *Service) processCanvasGenerationTask(ctx context.Context, userID string if strings.TrimSpace(input.Prompt) == "" { input.Prompt = fallbackPrompt } + if input.Mode == "" && strings.HasPrefix(taskType, "video_") { + input.Mode = "video" + } promptTemplateOperation := metadataString(input.Metadata, "promptTemplateOperation") - if promptTemplateOperation != "" { + // 视频节点的最终 Prompt 只取输入框内容,不能被分镜模板替换;图片和文本仍沿用模板能力。 + if input.Mode != "video" && promptTemplateOperation != "" { values := metadataStringValues(input.Metadata["promptTemplateVariables"]) compiled, compileErr := s.compilePrompt(userID, promptTemplateOperation, values) if compileErr != nil { @@ -174,15 +178,12 @@ func (s *Service) processCanvasGenerationTask(ctx context.Context, userID string if strings.TrimSpace(input.Prompt) == "" { return nil, errors.New("prompt is required") } - if input.Mode == "" && strings.HasPrefix(taskType, "video_") { - input.Mode = "video" - } config, err := s.resolveProviderConfig(input.Config) if err != nil { return nil, err } input.Config = config - if (input.Mode == "image" || input.Mode == "video") && input.Metadata != nil { + if input.Mode == "image" && input.Metadata != nil { if err := s.applyGenerationStyleProfile(userID, taskProjectID, &input); err != nil { return nil, err } diff --git a/web/src/components/canvas/canvas-node-generation.ts b/web/src/components/canvas/canvas-node-generation.ts index dbb1d7fd3..d7256b7e5 100644 --- a/web/src/components/canvas/canvas-node-generation.ts +++ b/web/src/components/canvas/canvas-node-generation.ts @@ -55,13 +55,13 @@ export type NodeGenerationInput = { character?: CharacterGenerationReference; }; -export function buildNodeGenerationContext(nodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[], prompt: string): NodeGenerationContext { +export function buildNodeGenerationContext(nodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[], prompt: string, promptOnly = false): NodeGenerationContext { const inputs = buildNodeGenerationInputs(nodeId, nodes, connections); const sourceNode = nodes.find((node) => node.id === nodeId); const storyboardInputs = getConnectedStoryboardRows(nodeId, nodes, connections); const hasExplicitNodeMention = /@\[node:[^\]]+\]/.test(normalizeLegacyNodeMentions(prompt, inputs)); if ((sourceNode?.type === CanvasNodeType.Config && Boolean(sourceNode.metadata?.composerContent?.trim())) || hasExplicitNodeMention) { - return buildComposerGenerationContext(inputs, prompt, [sourceNode?.metadata?.videoStartFrameNodeId, sourceNode?.metadata?.videoEndFrameNodeId].filter((id): id is string => Boolean(id))); + return buildComposerGenerationContext(inputs, prompt, [sourceNode?.metadata?.videoStartFrameNodeId, sourceNode?.metadata?.videoEndFrameNodeId].filter((id): id is string => Boolean(id)), promptOnly); } const isStoryboardMedia = sourceNode?.type === CanvasNodeType.Image || sourceNode?.type === CanvasNodeType.Video; @@ -77,7 +77,7 @@ export function buildNodeGenerationContext(nodeId: string, nodes: CanvasNodeData const referenceAudios = inputs.map((input) => input.audio).filter((audio): audio is ReferenceAudio => Boolean(audio)); return { - prompt: upstreamText ? `${basePrompt}\n\n${upstreamText}` : basePrompt, + prompt: promptOnly ? prompt : upstreamText ? `${basePrompt}\n\n${upstreamText}` : basePrompt, referenceImages, referenceVideos, referenceAudios, @@ -109,7 +109,7 @@ function removeTrailingInputBlocks(prompt: string, inputs: NodeGenerationInput[] return next; } -function buildComposerGenerationContext(inputs: NodeGenerationInput[], prompt: string, videoFrameNodeIds: string[] = []): NodeGenerationContext { +function buildComposerGenerationContext(inputs: NodeGenerationInput[], prompt: string, videoFrameNodeIds: string[] = [], promptOnly = false): NodeGenerationContext { const normalizedPrompt = normalizeLegacyNodeMentions(prompt, inputs); const inputByNodeId = new Map(inputs.map((input) => [input.nodeId, input])); const selectedInputs: NodeGenerationInput[] = []; @@ -140,7 +140,7 @@ function buildComposerGenerationContext(inputs: NodeGenerationInput[], prompt: s } nextPrompt += normalizedPrompt.slice(lastIndex); - if (textBlocks.length) nextPrompt = `${nextPrompt.trim()}\n\n${textBlocks.join("\n\n")}`; + if (textBlocks.length && !promptOnly) nextPrompt = `${nextPrompt.trim()}\n\n${textBlocks.join("\n\n")}`; // 首尾帧是结构化生成参数,不受提示词中的 @ 引用筛选影响。 const selectedNodeIds = new Set(selectedInputs.map((input) => input.nodeId)); videoFrameNodeIds.forEach((nodeId) => { @@ -286,7 +286,7 @@ export function buildNodeResponseMessages(context: NodeGenerationContext): AiTex ]; } -export async function hydrateNodeGenerationContext(context: NodeGenerationContext, projectId: string, domainProjectId?: string, mode?: CanvasGenerationMode, includeCharacterVoiceSamples = false) { +export async function hydrateNodeGenerationContext(context: NodeGenerationContext, projectId: string, domainProjectId?: string, mode?: CanvasGenerationMode, includeCharacterVoiceSamples = false, includeCharacterPrompt = true) { const { imageToDataUrl } = await import("@/services/image-storage"); let referenceImages = await Promise.all( context.referenceImages.map(async (image) => { @@ -365,7 +365,7 @@ export async function hydrateNodeGenerationContext(context: NodeGenerationContex const voiceBlocks = mode === "video" ? resolvedCharacterVoices.map(compileResolvedVoicePrompt) : []; return { ...context, - prompt: [context.prompt.trim(), ...characterBlocks, ...voiceBlocks].filter(Boolean).join("\n\n"), + prompt: includeCharacterPrompt ? [context.prompt.trim(), ...characterBlocks, ...voiceBlocks].filter(Boolean).join("\n\n") : context.prompt, referenceImages, referenceAudios, resolvedCharacterVersions, diff --git a/web/src/pages/canvas/use-canvas-generation-executor.ts b/web/src/pages/canvas/use-canvas-generation-executor.ts index a7bb2039b..ec37930bb 100644 --- a/web/src/pages/canvas/use-canvas-generation-executor.ts +++ b/web/src/pages/canvas/use-canvas-generation-executor.ts @@ -124,13 +124,16 @@ export function useCanvasGenerationExecutor({ } let rawGenerationContext: Awaited>; + // 视频文本只保留输入框内容;连接的媒体仍作为结构化参考传递。 + const promptOnly = mode === "video"; try { rawGenerationContext = await hydrateNodeGenerationContext( - buildNodeGenerationContext(nodeId, nodesRef.current, connectionsRef.current, editingTextNode ? `请根据要求修改以下文本。\n\n原文:\n${sourceTextContent}\n\n修改要求:\n${prompt}` : generationPrompt), + buildNodeGenerationContext(nodeId, nodesRef.current, connectionsRef.current, editingTextNode ? `请根据要求修改以下文本。\n\n原文:\n${sourceTextContent}\n\n修改要求:\n${prompt}` : generationPrompt, promptOnly), projectId, domainProjectId, mode, mode === "video" && supportsVideoReferenceAudio(generationConfig), + !promptOnly, ); } catch (error) { const errorDetails = generationErrorMessage(error); @@ -143,10 +146,10 @@ export function useCanvasGenerationExecutor({ return; } - const expandedPrompt = expandSkillMentions(rawGenerationContext.prompt, addedSkills); + const expandedPrompt = promptOnly ? rawGenerationContext.prompt : expandSkillMentions(rawGenerationContext.prompt, addedSkills); let effectivePrompt = expandedPrompt.trim(); let styleMetadata = {}; - if (mode === "image" || mode === "video") { + if (mode === "image") { try { const styleRuntime = resolveCanvasStyleExecution(nodesRef.current, sourceNode, effectivePrompt, generationConfig, mode); if (styleRuntime) { diff --git a/web/src/pages/canvas/use-canvas-generation-retry.ts b/web/src/pages/canvas/use-canvas-generation-retry.ts index 2df7e572e..5e82a4db2 100644 --- a/web/src/pages/canvas/use-canvas-generation-retry.ts +++ b/web/src/pages/canvas/use-canvas-generation-retry.ts @@ -86,15 +86,16 @@ export function useCanvasGenerationRetry({ projectId, domainProjectId, addedSkil } let rawContext: Awaited> | null; try { - 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)); + const promptOnly = retryMode === "video"; + const baseContext = buildNodeGenerationContext(sourceNode.id, nodesRef.current, connectionsRef.current, retryContextPrompt, promptOnly); + rawContext = hasSavedImageMetadata && !baseContext.characterReferences.length ? null : await hydrateNodeGenerationContext(baseContext, projectId, domainProjectId, retryMode, retryMode === "video" && supportsVideoReferenceAudio(generationConfig), !promptOnly); } catch (error) { const failure = generationFailureMetadata(error, retryPromptSource); message.error(failure.errorDetails); setNodes((current) => current.map((item) => (item.id === node.id ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, ...failure } } : item))); return; } - const context = rawContext ? { ...rawContext, prompt: expandSkillMentions(rawContext.prompt, addedSkills) } : null; + const context = rawContext ? { ...rawContext, prompt: retryMode === "video" ? rawContext.prompt : expandSkillMentions(rawContext.prompt, addedSkills) } : null; const prompt = (context?.characterReferences.length ? context.prompt : savedImageMetadata?.prompt || context?.prompt || "").trim(); if (!prompt) { message.warning("找不到提示词,无法重试"); @@ -102,9 +103,9 @@ export function useCanvasGenerationRetry({ projectId, domainProjectId, addedSkil } let mediaPrompt = prompt; let styleMetadata = {}; - if (node.type === CanvasNodeType.Image || node.type === CanvasNodeType.Video) { + if (node.type === CanvasNodeType.Image) { try { - const runtime = resolveCanvasStyleExecution(nodesRef.current, sourceNode, prompt, generationConfig, node.type === CanvasNodeType.Image ? "image" : "video"); + const runtime = resolveCanvasStyleExecution(nodesRef.current, sourceNode, prompt, generationConfig, "image"); if (runtime) { mediaPrompt = runtime.prompt; styleMetadata = { styleProfileJson: runtime.profileJson, styleExecutionPlan: runtime.plan };