feat(canvas): support multimodal video storyboard tables
This commit is contained in:
1 parent
7985dbec48
commit
6d355380cb
10 files changed
+425
-45
No files matched your search
@@ -95,7 +95,8 @@ import { CanvasLeaferGraphicsLayer } from "@/components/canvas/canvas-leafer-gra
|
||||
import { CanvasFreeformEmptyState, CanvasLinkedProjectEmptyState, CanvasShortDramaEmptyState, CanvasShortDramaGuide, CanvasStoryInputNodeContent, CanvasStylePlaceholderNodeContent } from "@/components/canvas/canvas-short-drama-entry";
|
||||
import { resolveCanvasEmptyStateKind } from "@/lib/canvas/canvas-starter";
|
||||
import { failedImageBatchChildren, markImageBatchRetrying, reconcileImageBatchRoot, restoreUnsubmittedImageBatchChild } from "@/lib/canvas/canvas-image-batch-retry";
|
||||
import { createCanvasNode, getInputSummary, isHiddenBatchChild } from "@/lib/canvas/canvas-project-domain";
|
||||
import { cinematicStoryboardColumns, createCanvasNode, getInputSummary, isHiddenBatchChild } from "@/lib/canvas/canvas-project-domain";
|
||||
import { storyboardRowsFromBatchTable } from "@/lib/canvas/canvas-batch-storyboard";
|
||||
import { stampCanvasNodeChanges, updateCanvasNode, updateCanvasNodes } from "@/lib/canvas/canvas-node-timestamps";
|
||||
import { canvasAssetHandoffAttempt, finalizeCanvasAssetHandoff, uninsertedCanvasAssetHandoffPayloads } from "@/lib/canvas/canvas-asset-handoff";
|
||||
import { batchSourceRestriction } from "@/lib/canvas/canvas-batch-connection";
|
||||
@@ -1988,6 +1989,60 @@ function InfiniteCanvasPage() {
|
||||
enqueueGenerationBatch,
|
||||
});
|
||||
|
||||
const createStoryboardFromBatchTable = useCallback(async (tableNodeId: string) => {
|
||||
const tableNode = nodesRef.current.find((node) => node.id === tableNodeId && node.type === CanvasNodeType.BatchTable);
|
||||
const table = tableNode?.metadata?.batchTable;
|
||||
if (!tableNode || !table || table.contentKind !== "storyboard") return;
|
||||
const rows = storyboardRowsFromBatchTable(table);
|
||||
if (!rows.length) return message.warning("当前表格没有可用的分镜行");
|
||||
const sourceIds = Array.from(new Set([
|
||||
...(table.storyboardSourceNodeIds || []),
|
||||
...connectionsRef.current.filter((connection) => connection.toNodeId === tableNodeId && connection.relation !== "batch-output").map((connection) => connection.fromNodeId),
|
||||
])).filter((id) => nodesRef.current.some((node) => node.id === id));
|
||||
const sourceVideo = sourceIds.map((id) => nodesRef.current.find((node) => node.id === id)).find((node): node is CanvasNodeData => node?.type === CanvasNodeType.Video);
|
||||
const keyframeTimesMs = rows.map((row) => row.keyframeTimeMs ?? (row.sourceStartMs !== undefined && row.sourceEndMs !== undefined ? (row.sourceStartMs + row.sourceEndMs) / 2 : undefined)).filter((value): value is number => Number.isFinite(value));
|
||||
const useKeyframes = Boolean(sourceVideo && keyframeTimesMs.length);
|
||||
const scriptSpec = getNodeSpec(CanvasNodeType.Script);
|
||||
const scriptNode = createCanvasNode(CanvasNodeType.Script, {
|
||||
x: tableNode.position.x + tableNode.width + 160 + scriptSpec.width / 2,
|
||||
y: tableNode.position.y + Math.max(tableNode.height, scriptSpec.height) / 2,
|
||||
}, {
|
||||
storyboard: { rows, visibleColumns: cinematicStoryboardColumns(), referenceNodeIds: sourceIds },
|
||||
storyboardVideoInputMode: useKeyframes ? "keyframe" : "direct",
|
||||
workflowKind: "storyboard",
|
||||
workflowTitle: table.storyboardTitle || "视频脚本",
|
||||
composerContent: table.storyboardTitle || "来自多维表格的视频分镜脚本",
|
||||
status: "idle",
|
||||
});
|
||||
scriptNode.title = table.storyboardTitle || "视频脚本";
|
||||
const nextConnections = [
|
||||
...connectionsRef.current,
|
||||
...sourceIds.map((sourceId) => ({ id: nanoid(), fromNodeId: sourceId, toNodeId: scriptNode.id, toHandleId: "storyboard:context", relation: "storyboard-asset-reference" as const })),
|
||||
];
|
||||
const nextNodes = [...nodesRef.current, scriptNode];
|
||||
nodesRef.current = nextNodes;
|
||||
connectionsRef.current = nextConnections;
|
||||
setNodes(nextNodes);
|
||||
setConnections(nextConnections);
|
||||
setSelectedNodeIds(new Set([scriptNode.id]));
|
||||
setDialogNodeId(scriptNode.id);
|
||||
if (sourceVideo && keyframeTimesMs.length) {
|
||||
const frameNodes = await extractVideoFrames(sourceVideo, { timesMs: keyframeTimesMs });
|
||||
if (frameNodes.length) {
|
||||
const frameByIndex = new Map(frameNodes.map((node, index) => [index, node.id]));
|
||||
const updatedRows = rows.map((row, index) => ({ ...row, imageNodeId: frameByIndex.get(index) || row.imageNodeId }));
|
||||
const withFrames = nodesRef.current.map((node) => node.id === scriptNode.id ? { ...node, metadata: { ...node.metadata, storyboard: { ...node.metadata?.storyboard, rows: updatedRows, visibleColumns: cinematicStoryboardColumns(), referenceNodeIds: sourceIds } } } : node);
|
||||
nodesRef.current = withFrames;
|
||||
setNodes(withFrames);
|
||||
message.success(`已创建视频脚本并自动抽取 ${frameNodes.length} 个关键帧,点击“生成视频”继续`);
|
||||
} else {
|
||||
message.warning("视频脚本已创建,但关键帧抽取失败;请检查视频是否已保存或允许跨域读取");
|
||||
}
|
||||
} else {
|
||||
message.success(`已创建视频脚本:${rows.length} 个镜头`);
|
||||
}
|
||||
}, [connectionsRef, extractVideoFrames, message, nodesRef, setConnections, setDialogNodeId, setNodes, setSelectedNodeIds]);
|
||||
|
||||
const handleRetryNode = useCanvasGenerationRetry({
|
||||
projectId,
|
||||
domainProjectId: currentProject?.projectId,
|
||||
@@ -2203,6 +2258,7 @@ function InfiniteCanvasPage() {
|
||||
onUpdateRow={(rowId, patch) => updateBatchRow(contentNode.id, rowId, patch)}
|
||||
onFillRows={() => fillRowsFromConnections(contentNode.id)}
|
||||
onGenerate={(rowIds) => void generateBatchRows(contentNode.id, rowIds)}
|
||||
onCreateStoryboard={() => createStoryboardFromBatchTable(contentNode.id)}
|
||||
onRetryItem={(batchId, itemId) => retryFailedBatchItems(contentNode.id, batchId, itemId)}
|
||||
onAddReferenceColumn={() => addBatchReferenceColumn(contentNode.id)}
|
||||
onRemoveReferenceColumn={() => removeBatchReferenceColumn(contentNode.id)}
|
||||
@@ -2290,6 +2346,7 @@ function InfiniteCanvasPage() {
|
||||
connections,
|
||||
createAndGenerateScriptVideos,
|
||||
createScriptActionBoards,
|
||||
createStoryboardFromBatchTable,
|
||||
createScriptImageNodes,
|
||||
createScriptVideoNodes,
|
||||
currentProject?.directorScenes,
|
||||
|
||||
Reference in new issue
Block a user