feat(*): 短剧创作工作台 - 完善章节分镜、资产管理、技能运行与生成任务恢复
This commit is contained in:
@@ -4,6 +4,7 @@ import type { NodeGenerationContext } from "@/components/canvas/canvas-node-gene
|
||||
import type { GenerationTask } from "@/services/api/task-center";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
import type { StyleExecutionPlan } from "@/lib/canvas/style-profile";
|
||||
import type { SkillRuntimeMetadata } from "@/services/skill-runtime";
|
||||
import type { CanvasConnection, CanvasNodeData } from "@/types/canvas";
|
||||
|
||||
export type CanvasGenerationExecutorDependencies = {
|
||||
@@ -32,6 +33,7 @@ export type CanvasGenerationExecution = CanvasGenerationExecutorDependencies & {
|
||||
controller: AbortController;
|
||||
editingTextNode: boolean;
|
||||
styleMetadata: { styleProfileJson?: string; styleExecutionPlan?: StyleExecutionPlan };
|
||||
skillMetadata: SkillRuntimeMetadata;
|
||||
taskContext?: { conversationId?: string; messageId?: string };
|
||||
retryContext?: { retryOf: string; attemptGroupId: string; clientOperationId: string };
|
||||
registerPendingNodeIds: (nodeIds: string[]) => void;
|
||||
|
||||
@@ -35,6 +35,7 @@ export async function executeImageGeneration({
|
||||
bindGenerationTask,
|
||||
applyGenerationTaskResult,
|
||||
styleMetadata,
|
||||
skillMetadata,
|
||||
taskContext,
|
||||
retryContext,
|
||||
showError,
|
||||
@@ -96,6 +97,7 @@ export async function executeImageGeneration({
|
||||
primaryImageId: undefined,
|
||||
...generationMetadata,
|
||||
...styleMetadata,
|
||||
...skillMetadata,
|
||||
imageBatchExpanded: count > 1 ? true : undefined,
|
||||
generationErrorCode: undefined,
|
||||
resourceReloadAvailable: undefined,
|
||||
@@ -116,6 +118,7 @@ export async function executeImageGeneration({
|
||||
batchRootId: count > 1 && !directCopiedBatch ? rootId : undefined,
|
||||
...generationMetadata,
|
||||
...styleMetadata,
|
||||
...skillMetadata,
|
||||
generationErrorCode: undefined,
|
||||
resourceReloadAvailable: undefined,
|
||||
failedPromptFingerprint: undefined,
|
||||
@@ -176,6 +179,7 @@ export async function executeImageGeneration({
|
||||
promptTemplateOperation: sourceNode?.metadata?.promptTemplateOperation,
|
||||
promptTemplateVariables: sourceNode?.metadata?.promptTemplateVariables,
|
||||
...styleMetadata,
|
||||
...skillMetadata,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ export async function executeVideoGeneration({
|
||||
applyGenerationTaskResult,
|
||||
registerPendingNodeIds,
|
||||
styleMetadata,
|
||||
skillMetadata,
|
||||
taskContext,
|
||||
retryContext,
|
||||
}: CanvasGenerationExecution) {
|
||||
@@ -62,6 +63,7 @@ export async function executeVideoGeneration({
|
||||
references: generationReferenceUrls(generationContext),
|
||||
...videoGenerationMetadata,
|
||||
...styleMetadata,
|
||||
...skillMetadata,
|
||||
},
|
||||
};
|
||||
registerPendingNodeIds([videoId]);
|
||||
@@ -111,6 +113,7 @@ export async function executeVideoGeneration({
|
||||
promptTemplateVariables: sourceNode?.metadata?.promptTemplateVariables,
|
||||
...videoGenerationMetadata,
|
||||
...styleMetadata,
|
||||
...skillMetadata,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -139,6 +142,7 @@ export async function executeAudioGeneration({
|
||||
applyGenerationTaskResult,
|
||||
registerPendingNodeIds,
|
||||
taskContext,
|
||||
skillMetadata,
|
||||
retryContext,
|
||||
}: CanvasGenerationExecution) {
|
||||
const spec = NODE_DEFAULT_SIZE[CanvasNodeType.Audio];
|
||||
@@ -152,7 +156,7 @@ export async function executeAudioGeneration({
|
||||
position: isEmptyAudioNode ? sourceNode.position : { x: parent.x + (sourceNode?.width || spec.width) + 96, y: parent.y + ((sourceNode?.height || spec.height) - spec.height) / 2 },
|
||||
width: isEmptyAudioNode ? sourceNode.width : spec.width,
|
||||
height: isEmptyAudioNode ? sourceNode.height : spec.height,
|
||||
metadata: { prompt: effectivePrompt, status: NODE_STATUS_LOADING, ...buildAudioGenerationMetadata(generationConfig) },
|
||||
metadata: { prompt: effectivePrompt, status: NODE_STATUS_LOADING, ...buildAudioGenerationMetadata(generationConfig), ...skillMetadata },
|
||||
};
|
||||
registerPendingNodeIds([audioId]);
|
||||
setNodes((current) =>
|
||||
@@ -171,7 +175,7 @@ export async function executeAudioGeneration({
|
||||
prompt: effectivePrompt,
|
||||
config: generationConfig,
|
||||
signal: controller.signal,
|
||||
metadata: { sourceNodeId: nodeId, ...taskContext, resolvedCharacterVersions: generationContext.resolvedCharacterVersions, resolvedCharacterVoiceKey: generationContext.resolvedCharacterVoices[0]?.voiceKey },
|
||||
metadata: { sourceNodeId: nodeId, ...taskContext, resolvedCharacterVersions: generationContext.resolvedCharacterVersions, resolvedCharacterVoiceKey: generationContext.resolvedCharacterVoices[0]?.voiceKey, ...skillMetadata },
|
||||
},
|
||||
{
|
||||
bindTask: (task) => bindGenerationTask(audioId, task),
|
||||
|
||||
@@ -27,6 +27,7 @@ export async function executeTextGeneration({
|
||||
applyGenerationTaskResult,
|
||||
registerPendingNodeIds,
|
||||
taskContext,
|
||||
skillMetadata,
|
||||
retryContext,
|
||||
}: CanvasGenerationExecution) {
|
||||
const isConfigNode = sourceNode?.type === CanvasNodeType.Config;
|
||||
@@ -50,7 +51,7 @@ export async function executeTextGeneration({
|
||||
},
|
||||
width: textConfig.width,
|
||||
height: textConfig.height,
|
||||
metadata: { prompt: effectivePrompt, status: NODE_STATUS_LOADING, fontSize: 14 },
|
||||
metadata: { prompt: effectivePrompt, status: NODE_STATUS_LOADING, fontSize: 14, ...skillMetadata },
|
||||
}));
|
||||
setNodes((current) => [...current.map((node) => (node.id === nodeId && isConfigNode ? { ...node, metadata: { ...node.metadata, prompt: effectivePrompt, status: NODE_STATUS_LOADING, errorDetails: undefined } } : node)), ...childNodes]);
|
||||
setConnections((current) => [...current, ...childIds.map((childId) => ({ id: nanoid(), fromNodeId: nodeId, toNodeId: childId }))]);
|
||||
@@ -71,7 +72,7 @@ export async function executeTextGeneration({
|
||||
referenceImages: generationContext.referenceImages,
|
||||
referenceVideos: generationContext.referenceVideos,
|
||||
signal: controller.signal,
|
||||
metadata: { sourceNodeId: nodeId, ...taskContext, resolvedCharacterVersions: generationContext.resolvedCharacterVersions },
|
||||
metadata: { sourceNodeId: nodeId, ...taskContext, resolvedCharacterVersions: generationContext.resolvedCharacterVersions, ...skillMetadata },
|
||||
},
|
||||
{
|
||||
bindTask: (task) => bindGenerationTask(targetNodeId, task),
|
||||
|
||||
@@ -1643,6 +1643,7 @@ function InfiniteCanvasPage() {
|
||||
const { addScriptRow, createAndGenerateScriptVideos, createScriptActionBoards, createScriptImageNodes, createScriptVideoNodes, generateScriptImages, generateScriptRows, generateScriptVideos, removeScriptRow, replaceScriptRows, updateScriptRow } =
|
||||
useCanvasStoryboard({
|
||||
projectId,
|
||||
addedSkills,
|
||||
nodesRef,
|
||||
connectionsRef,
|
||||
setNodes,
|
||||
|
||||
@@ -8,11 +8,11 @@ import { canvasGenerationRequestFingerprint, runCanvasGenerationSubmissionOnce }
|
||||
import { isGenerationTaskCapacityError } from "@/lib/canvas/canvas-generation-batch";
|
||||
import { buildPortraitTexturePrompt } from "@/lib/canvas/canvas-portrait-texture";
|
||||
import { resolveCanvasStyleExecution } from "@/lib/canvas/canvas-style-execution";
|
||||
import { expandSkillMentions } from "@/lib/canvas/canvas-skill-mentions";
|
||||
import { generationErrorMessage, generationFailureMetadata } from "@/lib/generation-error";
|
||||
import { modelCompatibilityError, modelGroupReferenceLimits, modelRequestOptions, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
import type { Skill } from "@/services/api/skills";
|
||||
import { skillRuntime } from "@/services/skill-runtime";
|
||||
import type { GenerationTask } from "@/services/api/task-center";
|
||||
import { useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
import type { Asset } from "@/stores/use-asset-store";
|
||||
@@ -161,8 +161,14 @@ export function useCanvasGenerationExecutor({
|
||||
return;
|
||||
}
|
||||
|
||||
const expandedPrompt = promptOnly ? rawGenerationContext.prompt : expandSkillMentions(rawGenerationContext.prompt, addedSkills);
|
||||
let effectivePrompt = expandedPrompt.trim();
|
||||
let skillExecution: Awaited<ReturnType<typeof skillRuntime.prepare<"canvas">>>;
|
||||
try {
|
||||
skillExecution = await skillRuntime.prepare({ profile: "canvas", prompt: rawGenerationContext.prompt, skills: addedSkills });
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "技能上下文加载失败");
|
||||
return;
|
||||
}
|
||||
let effectivePrompt = skillExecution.prompt.trim();
|
||||
let styleMetadata = {};
|
||||
if (mode === "image") {
|
||||
try {
|
||||
@@ -278,6 +284,7 @@ export function useCanvasGenerationExecutor({
|
||||
controller,
|
||||
editingTextNode,
|
||||
styleMetadata,
|
||||
skillMetadata: skillExecution.metadata,
|
||||
taskContext: options?.context,
|
||||
retryContext: options?.retryContext,
|
||||
setNodes,
|
||||
|
||||
@@ -23,13 +23,13 @@ import {
|
||||
supportsVideoReferenceAudio,
|
||||
} from "@/lib/canvas/canvas-project-generation";
|
||||
import { isCanvasWorkflowProvider } from "@/lib/canvas/canvas-workflow";
|
||||
import { expandSkillMentions } from "@/lib/canvas/canvas-skill-mentions";
|
||||
import { buildPortraitTexturePrompt } from "@/lib/canvas/canvas-portrait-texture";
|
||||
import { resolveCanvasStyleExecution } from "@/lib/canvas/canvas-style-execution";
|
||||
import { generationFailureMetadata, unchangedModeratedPrompt } from "@/lib/generation-error";
|
||||
import { modelCapabilityConfigFor } from "@/lib/model-capabilities";
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
import type { Skill } from "@/services/api/skills";
|
||||
import { skillRuntime, type SkillRuntimeMetadata } from "@/services/skill-runtime";
|
||||
import type { GenerationTask } from "@/services/api/task-center";
|
||||
import { resolveImageUrl } from "@/services/image-storage";
|
||||
import { resolveModelRequestConfig, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
@@ -122,7 +122,22 @@ export function useCanvasGenerationRetry({
|
||||
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: retryMode === "video" ? rawContext.prompt : expandSkillMentions(rawContext.prompt, addedSkills) } : null;
|
||||
let skillMetadata: SkillRuntimeMetadata = {
|
||||
skillIds: node.metadata?.skillIds || [],
|
||||
skillVersions: node.metadata?.skillVersions || [],
|
||||
skillFiles: node.metadata?.skillFiles || [],
|
||||
};
|
||||
let context = rawContext;
|
||||
if (rawContext) {
|
||||
try {
|
||||
const skillExecution = await skillRuntime.prepare({ profile: "canvas", prompt: rawContext.prompt, skills: addedSkills });
|
||||
context = { ...rawContext, prompt: skillExecution.prompt };
|
||||
skillMetadata = skillExecution.metadata;
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "技能上下文加载失败");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const prompt = (context?.characterReferences.length ? context.prompt : savedImageMetadata?.prompt || context?.prompt || "").trim();
|
||||
if (!prompt) {
|
||||
message.warning("找不到提示词,无法重试");
|
||||
@@ -252,6 +267,7 @@ export function useCanvasGenerationRetry({
|
||||
watermark: generationConfig.videoWatermark,
|
||||
...videoGenerationMetadata,
|
||||
...styleMetadata,
|
||||
...skillMetadata,
|
||||
references: videoContext ? generationReferenceUrls(videoContext) : item.metadata?.references,
|
||||
},
|
||||
}
|
||||
@@ -277,6 +293,7 @@ export function useCanvasGenerationRetry({
|
||||
promptTemplateVariables: node.metadata?.promptTemplateVariables,
|
||||
...videoGenerationMetadata,
|
||||
...styleMetadata,
|
||||
...skillMetadata,
|
||||
},
|
||||
});
|
||||
return;
|
||||
@@ -319,7 +336,7 @@ export function useCanvasGenerationRetry({
|
||||
item.id === node.id
|
||||
? {
|
||||
...item,
|
||||
metadata: { ...item.metadata, prompt: providerPrompt, ...generationMetadata, ...styleMetadata, emotionEdit: nextEmotionEdit },
|
||||
metadata: { ...item.metadata, prompt: providerPrompt, ...generationMetadata, ...styleMetadata, ...skillMetadata, emotionEdit: nextEmotionEdit },
|
||||
}
|
||||
: item,
|
||||
),
|
||||
@@ -333,7 +350,7 @@ export function useCanvasGenerationRetry({
|
||||
referenceImages: [editReference, characterReference],
|
||||
mask,
|
||||
signal: controller.signal,
|
||||
metadata: { retry: true, sourceNodeId: emotionSource.id, edit: "emotion", emotionEditMode: editPlan.mode, emotion: nextEmotionEdit, resolvedCharacterVersions: context?.resolvedCharacterVersions || [], ...styleMetadata },
|
||||
metadata: { retry: true, sourceNodeId: emotionSource.id, edit: "emotion", emotionEditMode: editPlan.mode, emotion: nextEmotionEdit, resolvedCharacterVersions: context?.resolvedCharacterVersions || [], ...styleMetadata, ...skillMetadata },
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -355,7 +372,7 @@ export function useCanvasGenerationRetry({
|
||||
item.id === node.id
|
||||
? {
|
||||
...item,
|
||||
metadata: { ...item.metadata, prompt: mediaPrompt, ...generationMetadata, ...styleMetadata },
|
||||
metadata: { ...item.metadata, prompt: mediaPrompt, ...generationMetadata, ...styleMetadata, ...skillMetadata },
|
||||
}
|
||||
: item,
|
||||
),
|
||||
@@ -375,6 +392,7 @@ export function useCanvasGenerationRetry({
|
||||
promptTemplateOperation: node.metadata?.promptTemplateOperation,
|
||||
promptTemplateVariables: node.metadata?.promptTemplateVariables,
|
||||
...styleMetadata,
|
||||
...skillMetadata,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -24,7 +24,9 @@ import { resolveStoryboardGenerationContext } from "@/lib/canvas/canvas-storyboa
|
||||
import { reconcileStoryboardTargetConnections, storyboardComposerContent, storyboardRowReferenceNodeIds } from "@/lib/canvas/canvas-storyboard-materializer";
|
||||
import { generationErrorMessage } from "@/lib/generation-error";
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
import type { Skill } from "@/services/api/skills";
|
||||
import { createGenerationTask, waitForGenerationTask } from "@/services/api/task-center";
|
||||
import { skillRuntime } from "@/services/skill-runtime";
|
||||
import { modelDisplayName, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
import {
|
||||
CanvasNodeType,
|
||||
@@ -36,6 +38,7 @@ import {
|
||||
|
||||
type UseCanvasStoryboardOptions = {
|
||||
projectId: string;
|
||||
addedSkills: Skill[];
|
||||
nodesRef: { current: CanvasNodeData[] };
|
||||
connectionsRef: { current: CanvasConnection[] };
|
||||
setNodes: Dispatch<SetStateAction<CanvasNodeData[]>>;
|
||||
@@ -51,6 +54,7 @@ const NODE_STATUS_ERROR = "error" as const;
|
||||
|
||||
export function useCanvasStoryboard({
|
||||
projectId,
|
||||
addedSkills,
|
||||
nodesRef,
|
||||
connectionsRef,
|
||||
setNodes,
|
||||
@@ -135,13 +139,18 @@ export function useCanvasStoryboard({
|
||||
navigateToSettings({ continueCreation: true });
|
||||
return;
|
||||
}
|
||||
setNodes((current) => current.map((node) => node.id === nodeId ? { ...node, metadata: { ...node.metadata, composerContent: prompt, status: NODE_STATUS_LOADING, taskStage: "正在创建任务", taskProgress: 0, errorDetails: undefined } } : node));
|
||||
try {
|
||||
const skillExecution = await skillRuntime.prepare({
|
||||
profile: "shortDrama",
|
||||
prompt: expandedPrompt,
|
||||
skills: addedSkills,
|
||||
});
|
||||
setNodes((current) => current.map((node) => node.id === nodeId ? { ...node, metadata: { ...node.metadata, composerContent: prompt, status: NODE_STATUS_LOADING, taskStage: "正在创建任务", taskProgress: 0, errorDetails: undefined, ...skillExecution.metadata } } : node));
|
||||
const task = await createGenerationTask({
|
||||
projectId,
|
||||
type: "agent_storyboard_rows",
|
||||
operation: "storyboard_rows",
|
||||
prompt: expandedPrompt,
|
||||
prompt: skillExecution.prompt,
|
||||
model: generationConfig.model,
|
||||
...(logicalModelIDForConfig(generationConfig) ? { logicalModelId: logicalModelIDForConfig(generationConfig) } : {}),
|
||||
input: {
|
||||
@@ -152,7 +161,7 @@ export function useCanvasStoryboard({
|
||||
shotDurationSeconds,
|
||||
shotCount: requestedShotCount,
|
||||
config: backendProviderConfig(generationConfig, "text"),
|
||||
metadata: { nodeId },
|
||||
metadata: { nodeId, ...skillExecution.metadata },
|
||||
},
|
||||
});
|
||||
setNodes((current) => current.map((node) => node.id === nodeId ? { ...node, metadata: { ...node.metadata, ...generationTaskMetadata(task), status: NODE_STATUS_LOADING } } : node));
|
||||
@@ -185,7 +194,7 @@ export function useCanvasStoryboard({
|
||||
message.error(details);
|
||||
return false;
|
||||
}
|
||||
}, [connectionsRef, effectiveConfig, isAiConfigReady, message, nodesRef, projectId, setNodes]);
|
||||
}, [addedSkills, connectionsRef, effectiveConfig, isAiConfigReady, message, nodesRef, projectId, setNodes]);
|
||||
|
||||
const ensureScriptImageNodes = useCallback((nodeId: string, rowIds: string[]) => {
|
||||
const scriptNode = nodesRef.current.find((node) => node.id === nodeId && node.type === CanvasNodeType.Script);
|
||||
|
||||
@@ -92,8 +92,13 @@ export function useCanvasViewportController({
|
||||
|
||||
const handleCanvasDoubleClick = useCallback((event: MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
if (!fitCanvasSelection()) fitCanvasContent();
|
||||
}, [fitCanvasContent, fitCanvasSelection]);
|
||||
event.stopPropagation();
|
||||
setSelectedNodeIds(new Set());
|
||||
setSelectedConnectionId(null);
|
||||
setDialogNodeId(null);
|
||||
setToolbarNodeId(null);
|
||||
setContextMenu({ type: "canvas", x: event.clientX, y: event.clientY, position: screenToCanvas(event.clientX, event.clientY), createOpen: true });
|
||||
}, [screenToCanvas, setContextMenu, setDialogNodeId, setSelectedConnectionId, setSelectedNodeIds, setToolbarNodeId]);
|
||||
|
||||
const selectFocusedNode = useCallback((nodeId: string) => {
|
||||
const selection = new Set([nodeId]);
|
||||
|
||||
Reference in New Issue
Block a user