71 lines
3.1 KiB
TypeScript
71 lines
3.1 KiB
TypeScript
|
|
import type { ToolDefinition } from './types';
|
|||
|
|
|
|||
|
|
export const validateStoryboard: ToolDefinition = {
|
|||
|
|
name: 'validate_storyboard',
|
|||
|
|
description: '校验分镜脚本质量:TTS 估算 ≤ 6s、ratio 预检、script 拼接校验。返回校验结果和问题列表。',
|
|||
|
|
input_schema: {
|
|||
|
|
type: 'object',
|
|||
|
|
properties: {
|
|||
|
|
items: { type: 'string', description: '分镜 JSON 数组字符串,每个元素需包含 shotDesc、script 字段,可选 duration、directorRef' },
|
|||
|
|
videoModelDuration: { type: 'number', description: '视频模型固定时长(秒),默认 6' },
|
|||
|
|
},
|
|||
|
|
required: ['items'],
|
|||
|
|
},
|
|||
|
|
execute: async (params) => {
|
|||
|
|
const { items, videoModelDuration = 6 } = params as { items: string; videoModelDuration?: number };
|
|||
|
|
let parsed: any[];
|
|||
|
|
try { parsed = JSON.parse(items); } catch { return '错误: items 不是合法 JSON'; }
|
|||
|
|
if (!Array.isArray(parsed) || parsed.length === 0) return '错误: items 必须是非空数组';
|
|||
|
|
|
|||
|
|
const errors: string[] = [];
|
|||
|
|
const warnings: string[] = [];
|
|||
|
|
const TTS_SPEED = 1.15;
|
|||
|
|
const CHARS_PER_SEC = 5;
|
|||
|
|
|
|||
|
|
for (const item of parsed) {
|
|||
|
|
const idx = item.id ?? parsed.indexOf(item) + 1;
|
|||
|
|
const script: string = item.script || '';
|
|||
|
|
const charCount = script.length;
|
|||
|
|
const ttsEstimate = charCount / CHARS_PER_SEC;
|
|||
|
|
const audioDur = ttsEstimate * TTS_SPEED;
|
|||
|
|
const ratio = videoModelDuration / audioDur;
|
|||
|
|
|
|||
|
|
if (!item.shotDesc) errors.push(`Shot ${idx}: 缺少 shotDesc`);
|
|||
|
|
if (!script) errors.push(`Shot ${idx}: 缺少 script`);
|
|||
|
|
|
|||
|
|
// TTS 估算检查
|
|||
|
|
if (ttsEstimate > 6) {
|
|||
|
|
errors.push(`Shot ${idx}: TTS 估算 ${ttsEstimate.toFixed(1)}s > 6s,必须拆分 (script: ${script.slice(0, 30)}...)`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ratio 预检
|
|||
|
|
if (ratio < 0.9) {
|
|||
|
|
errors.push(`Shot ${idx}: ratio ${ratio.toFixed(2)} < 0.9,音频太长需拆分 (audio=${audioDur.toFixed(1)}s, video=${videoModelDuration}s)`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!item.directorRef) {
|
|||
|
|
warnings.push(`Shot ${idx}: 建议填写 directorRef`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// script 拼接校验 - 返回统计而非原文比对(原文由用户提供)
|
|||
|
|
const totalChars = parsed.reduce((sum: number, i: any) => sum + (i.script?.length || 0), 0);
|
|||
|
|
const totalAudio = (totalChars / CHARS_PER_SEC) * TTS_SPEED;
|
|||
|
|
|
|||
|
|
const result = {
|
|||
|
|
valid: errors.length === 0,
|
|||
|
|
shotCount: parsed.length,
|
|||
|
|
totalChars,
|
|||
|
|
estimatedTotalAudio: `${totalAudio.toFixed(1)}s`,
|
|||
|
|
errors,
|
|||
|
|
warnings,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (errors.length > 0) {
|
|||
|
|
return `❌ 分镜校验未通过 (${errors.length} 个问题):\n\n${errors.map((e, i) => `${i + 1}. ${e}`).join('\n')}${warnings.length ? `\n\n⚠️ 警告:\n${warnings.map((w, i) => `${i + 1}. ${w}`).join('\n')}` : ''}\n\n统计: ${parsed.length} 个镜头, 总字数 ${totalChars}, 音频估算 ${totalAudio.toFixed(1)}s`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return `✅ 分镜校验通过\n\n统计: ${parsed.length} 个镜头, 总字数 ${totalChars}, 音频估算 ${totalAudio.toFixed(1)}s${warnings.length ? `\n\n⚠️ 警告:\n${warnings.map((w, i) => `${i + 1}. ${w}`).join('\n')}` : ''}`;
|
|||
|
|
},
|
|||
|
|
};
|