92 lines
3.9 KiB
TypeScript
92 lines
3.9 KiB
TypeScript
import type { AgentRunSnapshot } from "./protocol";
|
||
import { videoNodeType, videoNodeUrl, type VideoRuntimeNode } from "./video-protocol";
|
||
|
||
export async function verifyVideoAgentResults(params: {
|
||
snapshot: AgentRunSnapshot;
|
||
nodes: VideoRuntimeNode[];
|
||
inspect: (
|
||
node: VideoRuntimeNode,
|
||
requirements: string,
|
||
) => Promise<{
|
||
passed: boolean;
|
||
issues: string[];
|
||
duration?: number;
|
||
width?: number;
|
||
height?: number;
|
||
}>;
|
||
}) {
|
||
const { snapshot } = params;
|
||
const requirements = snapshot.videoRequirements;
|
||
const produced = snapshot.resultNodeIds.map((id) => params.nodes.find((node) => node.id === id));
|
||
const issues: string[] = [];
|
||
if (produced.some((node) => !node)) issues.push("部分结果节点已不存在,请核对画布");
|
||
const videos = produced.filter((node): node is VideoRuntimeNode =>
|
||
Boolean(node && videoNodeType(node) === "video"),
|
||
);
|
||
const scripts = produced.filter((node) => node && ["script", "text"].includes(node.kind));
|
||
const plan = snapshot.calls.find(
|
||
(call) => call.name === "planCanvasTask" && call.status === "succeeded",
|
||
)?.output;
|
||
const expected = requirements?.expectedVideoCount ?? Number(plan?.expectedVideoCount || 1);
|
||
if (requirements?.expectsVideo && videos.length !== expected)
|
||
issues.push(`需要 ${expected} 条视频,当前仅完成 ${videos.length} 条;脚本和关键帧不计为成片`);
|
||
if (requirements?.expectsScript && !scripts.length) issues.push("尚未交付分镜脚本节点");
|
||
if (
|
||
!produced.length &&
|
||
!snapshot.calls.some(
|
||
(call) =>
|
||
["analyzeVideoMaterials", "editVideoCanvas", "listVideoCanvasCapabilities"].includes(
|
||
call.name,
|
||
) && call.status === "succeeded",
|
||
)
|
||
)
|
||
issues.push("尚未交付任何创作或分析结果");
|
||
for (const video of videos) {
|
||
if (!videoNodeUrl(video)) {
|
||
issues.push(`${video.title} 缺少可用视频地址`);
|
||
continue;
|
||
}
|
||
if (requirements?.duration && Number(video.draftValues?.duration) !== requirements.duration)
|
||
issues.push(`${video.title} 生成时长未采用最新要求 ${requirements.duration} 秒`);
|
||
if (requirements?.ratio && video.draftValues?.ratio !== requirements.ratio)
|
||
issues.push(`${video.title} 生成比例未采用最新要求 ${requirements.ratio}`);
|
||
try {
|
||
const result = await params.inspect(
|
||
video,
|
||
[snapshot.objective, ...snapshot.inputs.map((input) => input.text)].join("\n"),
|
||
);
|
||
if (!result.duration || !result.width || !result.height)
|
||
issues.push(`${video.title} 缺少实际视频时长或尺寸,不能仅按请求参数验收`);
|
||
const duration =
|
||
requirements?.duration ||
|
||
(video.draftValues?.mediaOperation ? undefined : snapshot.videoSettings?.duration);
|
||
if (duration && result.duration && Math.abs(result.duration - duration) > 1)
|
||
issues.push(
|
||
`${video.title} 实际时长 ${result.duration.toFixed(1)} 秒,不符合 ${duration} 秒要求`,
|
||
);
|
||
const ratio = (
|
||
requirements?.ratio ||
|
||
(video.draftValues?.mediaOperation ? undefined : snapshot.videoSettings?.ratio) ||
|
||
""
|
||
)
|
||
.split(":")
|
||
.map(Number);
|
||
if (
|
||
result.width &&
|
||
result.height &&
|
||
ratio.length === 2 &&
|
||
Math.abs(result.width / result.height - ratio[0] / ratio[1]) > 0.05
|
||
)
|
||
issues.push(`${video.title} 实际尺寸 ${result.width}×${result.height} 不符合指定比例`);
|
||
if (!result.passed || result.issues.length)
|
||
issues.push(
|
||
...(result.issues.length ? result.issues : [`${video.title} 的画面内容需要检查`]),
|
||
);
|
||
} catch {
|
||
issues.push(`${video.title} 暂时无法读取或完成画面检查;已保留生成结果,请稍后核对`);
|
||
}
|
||
}
|
||
// A failed visual review must never trigger another paid video automatically.
|
||
return { passed: issues.length === 0, issues: issues.slice(0, 12), retryable: false };
|
||
}
|