Files
ttstd-infinite-canvas/web/src/pages/canvas/canvas-media-generation-executors.ts
T

193 lines
9.3 KiB
TypeScript

import { nanoid } from "nanoid";
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
import { canGenerateMediaInPlace } from "@/lib/canvas/canvas-generation-layout";
import { nodeSizeFromRatio } from "@/lib/canvas/canvas-node-size";
import { nextCanvasVersionLabel } from "@/lib/canvas/canvas-layout";
import { buildAudioGenerationMetadata, buildVideoGenerationMetadata, generationReferenceUrls, runCanvasGenerationTaskToConsumer } from "@/lib/canvas/canvas-project-generation";
import { canvasGenerationPromptMetadata } from "@/lib/canvas/canvas-generation-submission";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
import type { CanvasGenerationExecution } from "./canvas-generation-executor-types";
const NODE_STATUS_LOADING = "loading" as const;
const NODE_STATUS_SUCCESS = "success" as const;
export async function executeVideoGeneration({
nodeId,
sourceNode,
prompt,
effectivePrompt,
generationConfig,
generationContext,
controller,
projectId,
canvasConnections,
setNodes,
setConnections,
startGenerationRequest,
finishGenerationRequest,
bindGenerationTask,
applyGenerationTaskResult,
registerPendingNodeIds,
styleMetadata,
skillMetadata,
taskContext,
retryContext,
}: CanvasGenerationExecution) {
const spec = nodeSizeFromRatio(generationConfig.size, NODE_DEFAULT_SIZE[CanvasNodeType.Video].width, NODE_DEFAULT_SIZE[CanvasNodeType.Video].height) || NODE_DEFAULT_SIZE[CanvasNodeType.Video];
const reuseSourceNode = canGenerateMediaInPlace(sourceNode, CanvasNodeType.Video);
const isExistingVideoNode = sourceNode?.type === CanvasNodeType.Video && Boolean(sourceNode.metadata?.content) && !reuseSourceNode;
const videoId = reuseSourceNode ? nodeId : nanoid();
const versionRootId = isExistingVideoNode && sourceNode ? sourceNode.metadata?.versionOfNodeId || sourceNode.id : undefined;
const parent = sourceNode?.position || { x: 0, y: 0 };
const videoGenerationMetadata = buildVideoGenerationMetadata(sourceNode, generationContext, generationConfig);
const videoNode: CanvasNodeData = {
id: videoId,
type: CanvasNodeType.Video,
title: effectivePrompt.slice(0, 32) || "Generated Video",
position: reuseSourceNode ? sourceNode!.position : { x: parent.x + (sourceNode?.width || spec.width) + 96, y: parent.y },
width: reuseSourceNode ? sourceNode!.width : spec.width,
height: reuseSourceNode ? sourceNode!.height : spec.height,
metadata: {
...(reuseSourceNode ? sourceNode?.metadata || {} : {}),
...canvasGenerationPromptMetadata(prompt, effectivePrompt),
status: NODE_STATUS_LOADING,
errorDetails: undefined,
generationErrorCode: undefined,
resourceReloadAvailable: undefined,
failedPromptFingerprint: undefined,
model: generationConfig.model,
size: generationConfig.size,
seconds: generationConfig.videoSeconds,
vquality: generationConfig.vquality,
generateAudio: generationConfig.videoGenerateAudio,
watermark: generationConfig.videoWatermark,
references: generationReferenceUrls(generationContext),
...videoGenerationMetadata,
...styleMetadata,
...skillMetadata,
},
};
registerPendingNodeIds([videoId]);
// 待生成版本先加入版本族,但只有成功结果才能替换当前主版本。
setNodes((current) => {
if (reuseSourceNode) return current.map((node) => (node.id === nodeId ? { ...node, ...videoNode } : node));
if (!isExistingVideoNode || !sourceNode) return [...current.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_SUCCESS } } : node)), videoNode];
const rootId = versionRootId!;
const nextLabel = nextCanvasVersionLabel(rootId, current);
const hasPrimaryVersion = current.some((node) => (node.metadata?.versionOfNodeId || node.id) === rootId && node.metadata?.versionPrimary);
return [
...current.map((node) => {
if ((node.metadata?.versionOfNodeId || node.id) !== rootId) return node;
return { ...node, metadata: { ...node.metadata, versionOfNodeId: rootId, versionLabel: node.metadata?.versionLabel || "A", versionPrimary: node.metadata?.versionPrimary || (!hasPrimaryVersion && node.id === sourceNode.id), status: node.id === nodeId ? NODE_STATUS_SUCCESS : node.metadata?.status } };
}),
{ ...videoNode, metadata: { ...videoNode.metadata, versionOfNodeId: rootId, versionLabel: nextLabel, versionPrimary: false } },
];
});
// 重新生成已有视频时,新节点继承源视频的上游连接,与源视频保持并行关系,而不是作为其下游子节点。
if (!reuseSourceNode) {
setConnections((current) => {
if (!isExistingVideoNode) return [...current, { id: nanoid(), fromNodeId: nodeId, toNodeId: videoId }];
return [...current, ...canvasConnections.filter((connection) => connection.toNodeId === nodeId).map((connection) => ({ ...connection, id: nanoid(), toNodeId: videoId }))];
});
}
startGenerationRequest(videoId, nodeId, nodeId, controller);
try {
await runCanvasGenerationTaskToConsumer(
{
projectId,
nodeId: videoId,
...retryContext,
mode: "video",
prompt: effectivePrompt,
config: generationConfig,
referenceImages: generationContext.referenceImages,
referenceVideos: generationContext.referenceVideos,
referenceAudios: generationContext.referenceAudios,
signal: controller.signal,
metadata: {
sourceNodeId: nodeId,
...taskContext,
resolvedCharacterVersions: generationContext.resolvedCharacterVersions,
resolvedCharacterVoices: generationContext.resolvedCharacterVoices,
promptTemplateOperation: sourceNode?.metadata?.promptTemplateOperation,
promptTemplateVariables: sourceNode?.metadata?.promptTemplateVariables,
...videoGenerationMetadata,
...styleMetadata,
...skillMetadata,
},
},
{
bindTask: (task) => bindGenerationTask(videoId, task),
consumeTask: (task) => applyGenerationTaskResult(videoId, task),
},
);
} finally {
finishGenerationRequest(videoId, controller);
}
}
export async function executeAudioGeneration({
nodeId,
sourceNode,
prompt,
effectivePrompt,
generationConfig,
generationContext,
controller,
projectId,
setNodes,
setConnections,
startGenerationRequest,
finishGenerationRequest,
bindGenerationTask,
applyGenerationTaskResult,
registerPendingNodeIds,
taskContext,
skillMetadata,
retryContext,
}: CanvasGenerationExecution) {
const spec = NODE_DEFAULT_SIZE[CanvasNodeType.Audio];
const isEmptyAudioNode = sourceNode?.type === CanvasNodeType.Audio && !sourceNode.metadata?.content;
const audioId = isEmptyAudioNode ? nodeId : nanoid();
const parent = sourceNode?.position || { x: 0, y: 0 };
const audioNode: CanvasNodeData = {
id: audioId,
type: CanvasNodeType.Audio,
title: effectivePrompt.slice(0, 32) || "Generated Audio",
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: { ...canvasGenerationPromptMetadata(prompt, effectivePrompt), status: NODE_STATUS_LOADING, ...buildAudioGenerationMetadata(generationConfig), ...skillMetadata },
};
registerPendingNodeIds([audioId]);
setNodes((current) =>
isEmptyAudioNode ? current.map((node) => (node.id === nodeId ? { ...node, ...audioNode } : node)) : [...current.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_SUCCESS } } : node)), audioNode],
);
if (!isEmptyAudioNode) setConnections((current) => [...current, { id: nanoid(), fromNodeId: nodeId, toNodeId: audioId }]);
startGenerationRequest(audioId, nodeId, nodeId, controller);
try {
await runCanvasGenerationTaskToConsumer(
{
projectId,
nodeId: audioId,
...retryContext,
mode: "audio",
prompt: effectivePrompt,
config: generationConfig,
signal: controller.signal,
metadata: { sourceNodeId: nodeId, ...taskContext, resolvedCharacterVersions: generationContext.resolvedCharacterVersions, resolvedCharacterVoiceKey: generationContext.resolvedCharacterVoices[0]?.voiceKey, ...skillMetadata },
},
{
bindTask: (task) => bindGenerationTask(audioId, task),
consumeTask: (task) => applyGenerationTaskResult(audioId, task),
},
);
} finally {
finishGenerationRequest(audioId, controller);
}
}