* feat(canvas): 视频节点截取重生成 - 支持多段/时间线导入并调用视频模型批量重生成 * test(canvas): 视频节点时间线导入 - 修正旧数据标题回退测试 * style(canvas): 视频节点截取重生成 - 格式化新增文件并修正测试 --------- Co-authored-by: sikongyue <3155157671@qq.com>
55 lines
3.1 KiB
TypeScript
55 lines
3.1 KiB
TypeScript
import { collectUpstreamVideoNodes } from "@/lib/canvas/canvas-resource-references";
|
|
import type { CanvasConnection, CanvasNodeData } from "@/types/canvas";
|
|
import type { TimelineClip, TimelineProject } from "@/types/timeline";
|
|
|
|
export type CanvasTimelineSegmentItem = {
|
|
id: string;
|
|
startMs: number;
|
|
endMs: number;
|
|
sourceNodeId?: string;
|
|
sourceStorageKey?: string;
|
|
sourceUrl?: string;
|
|
};
|
|
|
|
export type TimelineImportResult = { ok: true; segments: CanvasTimelineSegmentItem[] } | { ok: false; error: string };
|
|
|
|
const MIN_TIMELINE_SEGMENT_MS = 100;
|
|
|
|
export function buildTimelineImportSegments(node: CanvasNodeData, nodes: CanvasNodeData[], connections: CanvasConnection[], timeline: TimelineProject | null | undefined, durationMs: number): TimelineImportResult {
|
|
const candidateNodes = collectUpstreamVideoNodes(node.id, nodes, connections);
|
|
const matchesCandidate = (clip: TimelineClip, candidate: CanvasNodeData) => {
|
|
if (candidate.id === clip.nodeId) return true;
|
|
if (candidate.metadata?.assetId && clip.nodeId === candidate.metadata.assetId) return true;
|
|
if (candidate.metadata?.storageKey && clip.directMedia?.storageKey === candidate.metadata.storageKey) return true;
|
|
// 旧数据里 nodeId 可能被重新生成过;标题一致且唯一时才回退,避免同名视频误导入。
|
|
if (candidate.title && clip.title === candidate.title && !clip.directMedia) {
|
|
return candidateNodes.filter((item) => item.title === candidate.title).length === 1;
|
|
}
|
|
return false;
|
|
};
|
|
const clips = (timeline?.clips || []).filter((clip) => clip.kind === "video" && candidateNodes.some((candidate) => matchesCandidate(clip, candidate)));
|
|
if (!clips.length) {
|
|
return { ok: false, error: `未找到「${node.title || "当前视频节点"}」或其上游视频节点在时间线中的片段,请先在时间线中加入对应视频节点` };
|
|
}
|
|
const segments = clips.flatMap((clip) => {
|
|
const matchedNode = candidateNodes.find((candidate) => matchesCandidate(clip, candidate));
|
|
const startMs = Math.max(0, Math.round(clip.sourceStartMs ?? 0));
|
|
const rawEndMs = startMs + Math.max(MIN_TIMELINE_SEGMENT_MS, Math.round(clip.sourceDurationMs || clip.durationMs));
|
|
const sourceDurationMs = matchedNode?.metadata?.durationMs || durationMs;
|
|
const endMs = sourceDurationMs > 0 ? Math.min(sourceDurationMs, rawEndMs) : rawEndMs;
|
|
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) return [];
|
|
return [
|
|
{
|
|
id: `timeline-${clip.id}`,
|
|
startMs,
|
|
endMs,
|
|
sourceNodeId: matchedNode?.id,
|
|
sourceStorageKey: clip.directMedia?.storageKey || matchedNode?.metadata?.storageKey,
|
|
sourceUrl: clip.directMedia?.url || matchedNode?.metadata?.content,
|
|
},
|
|
];
|
|
});
|
|
if (!segments.length) return { ok: false, error: "时间线片段数据不完整,请先在时间线中重新分割或保存" };
|
|
return { ok: true, segments };
|
|
}
|