Files
ttstd-infinite-canvas/web/src/lib/canvas/canvas-video-timeline-segments.ts
爱笑的毛毛虫 eceaf7ac41 feat(canvas): 视频节点截取重生成 - 支持多段/时间线导入并调用视频模型批量重生成 (#197)
* feat(canvas): 视频节点截取重生成 - 支持多段/时间线导入并调用视频模型批量重生成

* test(canvas): 视频节点时间线导入 - 修正旧数据标题回退测试

* style(canvas): 视频节点截取重生成 - 格式化新增文件并修正测试

---------

Co-authored-by: sikongyue <3155157671@qq.com>
2026-08-10 00:13:02 +08:00

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 };
}