Files
ttstd-infinite-canvas/web/src/lib/canvas/canvas-storyboard-progress.ts

112 lines
5.2 KiB
TypeScript

import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type StoryboardRow } from "@/types/canvas";
export type StoryboardPipelineItemState = "missing" | "idle" | "loading" | "success" | "error";
export type StoryboardPipelineStage = {
total: number;
created: number;
success: number;
failed: number;
loading: number;
incomplete: number;
nodeIds: string[];
};
export type StoryboardPipelineRow = {
row: StoryboardRow;
imageNode?: CanvasNodeData;
imageState: StoryboardPipelineItemState;
videoNode?: CanvasNodeData;
videoState: StoryboardPipelineItemState;
};
export type CanvasStoryboardPipelineProgress = {
rows: StoryboardPipelineRow[];
images: StoryboardPipelineStage;
videos: StoryboardPipelineStage;
final: StoryboardPipelineStage;
successfulVideoNodeIds: string[];
finalNodeIds: string[];
};
export function deriveStoryboardPipelineProgress(scriptNode: CanvasNodeData, nodes: CanvasNodeData[], connections: CanvasConnection[]): CanvasStoryboardPipelineProgress {
const rows = scriptNode.metadata?.storyboard?.rows || [];
const nodeById = new Map(nodes.map((node) => [node.id, node]));
const pipelineRows = rows.map((row): StoryboardPipelineRow => {
const imageNode = row.imageNodeId ? nodeById.get(row.imageNodeId) : undefined;
const videoNode = row.videoNodeId ? nodeById.get(row.videoNodeId) : undefined;
return {
row,
imageNode: imageNode?.type === CanvasNodeType.Image ? imageNode : undefined,
imageState: nodePipelineState(imageNode, CanvasNodeType.Image),
videoNode: videoNode?.type === CanvasNodeType.Video ? videoNode : undefined,
videoState: nodePipelineState(videoNode, CanvasNodeType.Video),
};
});
const imageNodes = pipelineRows.flatMap((item) => item.imageNode ? [item.imageNode] : []);
const videoNodes = pipelineRows.flatMap((item) => item.videoNode ? [item.videoNode] : []);
const videoNodeIds = new Set(videoNodes.map((node) => node.id));
const linkedFinalNodes = nodes.filter((node) => node.type === CanvasNodeType.Video
&& node.metadata?.workflowKind === "final"
&& connections.some((connection) => connection.toNodeId === node.id && (connection.fromNodeId === scriptNode.id || videoNodeIds.has(connection.fromNodeId))));
const successfulVideoNodeIds = pipelineRows.flatMap((item) => item.videoState === "success" && item.videoNode ? [item.videoNode.id] : []);
return {
rows: pipelineRows,
images: summarizeStage(pipelineRows.map((item) => ({ state: item.imageState, node: item.imageNode })), rows.length),
videos: summarizeStage(pipelineRows.map((item) => ({ state: item.videoState, node: item.videoNode })), rows.length),
final: summarizeFinalStage(linkedFinalNodes, rows.length > 0 || linkedFinalNodes.length > 0),
successfulVideoNodeIds,
finalNodeIds: linkedFinalNodes.map((node) => node.id),
};
}
function nodePipelineState(node: CanvasNodeData | undefined, expectedType: CanvasNodeType): StoryboardPipelineItemState {
if (!node || node.type !== expectedType) return "missing";
if (node.metadata?.status === "success" && node.metadata.content) return "success";
if (node.metadata?.status === "loading") return "loading";
if (node.metadata?.status === "error") return "error";
return "idle";
}
function summarizeStage(items: Array<{ state: StoryboardPipelineItemState; node?: CanvasNodeData }>, total: number): StoryboardPipelineStage {
const success = items.filter((item) => item.state === "success").length;
const failed = items.filter((item) => item.state === "error").length;
const loading = items.filter((item) => item.state === "loading").length;
const created = items.filter((item) => Boolean(item.node)).length;
return {
total,
created,
success,
failed,
loading,
incomplete: Math.max(0, total - success),
nodeIds: items.flatMap((item) => item.node ? [item.node.id] : []),
};
}
function summarizeFinalStage(nodes: CanvasNodeData[], enabled: boolean): StoryboardPipelineStage {
if (!enabled) return { total: 0, created: 0, success: 0, failed: 0, loading: 0, incomplete: 0, nodeIds: [] };
const states = nodes.map((node) => nodePipelineState(node, CanvasNodeType.Video));
const success = states.includes("success") ? 1 : 0;
const loading = success ? 0 : states.includes("loading") ? 1 : 0;
const failed = success || loading ? 0 : states.includes("error") ? 1 : 0;
return {
total: 1,
created: nodes.length ? 1 : 0,
success,
failed,
loading,
incomplete: success ? 0 : 1,
nodeIds: nodes.map((node) => node.id),
};
}
export function pipelineStatusLabel(stage: StoryboardPipelineStage) {
if (!stage.total) return "待开始";
if (stage.success >= stage.total) return "已完成";
if (stage.loading) return `${stage.success}/${stage.total} · 进行中`;
if (stage.failed) return `${stage.success}/${stage.total} · 失败 ${stage.failed}`;
if (stage.created) return `${stage.success}/${stage.total} · 已创建 ${stage.created}`;
return `0/${stage.total}`;
}