import { z } from "zod"; import type { EditCandidate } from "./contract"; export const MIN_SEQUENCE_MATCH_SCORE = 60; export class HighlightValidationError extends Error { constructor(message: string) { super(message); this.name = "HighlightValidationError"; } } const highlightResultSchema = z.object({ selections: z.array(z.object({ id: z.string(), startSeconds: z.number().finite().min(0), transition: z.enum(["cut", "fade"]), reason: z.string().trim().min(1).max(300), matchScore: z.number().finite().min(0).max(100), visualIssues: z.array(z.string().trim().min(1).max(160)).max(10).default([]), })).min(1).max(12), summary: z.string().max(1000).default(""), }); export type SequenceHighlight = z.infer["selections"][number]; export function validateSequenceHighlights(value: unknown, candidates: EditCandidate[], shotDurations: number[]) { const parsed = highlightResultSchema.safeParse(value); if (!parsed.success) throw new HighlightValidationError("视频高光分析格式无效,未提交成片合成"); const result = parsed.data; if (shotDurations.length !== candidates.length || result.selections.length !== candidates.length || new Set(result.selections.map(item => item.id)).size !== candidates.length || result.selections.some((item, index) => item.id !== candidates[index].id)) throw new HighlightValidationError("视频高光分析遗漏、重复或更改了分镜顺序,未提交成片合成"); for (const [index, item] of result.selections.entries()) { const available = (candidates[index].end - candidates[index].start) / 1_000_000; const wanted = shotDurations[index]; if (!Number.isFinite(wanted) || wanted <= 0 || item.startSeconds + wanted > available + 0.001) throw new HighlightValidationError(`镜头 ${index + 1} 的高光区间超出真实视频长度,请重新选片`); if (index === 0 && item.transition !== "cut") throw new HighlightValidationError("首镜头的转场分析无效,请重新选片"); if (item.visualIssues.length) throw new HighlightValidationError(`镜头 ${index + 1} 存在画面违规:${item.visualIssues.join(";")}。请替换或重新生成该镜头后再合成`); if (item.matchScore < MIN_SEQUENCE_MATCH_SCORE) throw new HighlightValidationError(`镜头 ${index + 1} 未达到需求匹配门槛(${item.matchScore}/${MIN_SEQUENCE_MATCH_SCORE}):${item.reason}。请替换或重新生成该镜头后再合成`); } return result; }