- 引入 manifest.json 作为唯一状态源,所有子 Agent 操作回写 manifest - 重构 timebuilder 逻辑,支持四种视频适配策略(加速/裁剪/放缓/画面停顿) - 统一 TTS 阶段输出结构,单句和多句均写入 segments[] - 重写字幕和配音生成,基于 segments 精确时长实现音画同步 - 新增 confirm 命令支持按 id 范围确认,上传阶段分离图片和视频 - 添加中间产物写入 output/ 目录的约束,清理废弃配置参数
120 lines
4.5 KiB
JavaScript
120 lines
4.5 KiB
JavaScript
/**
|
||
* Command: validate — 校验 manifest.json 完整性
|
||
* Command: validate-account — 校验账号目录完整性
|
||
*/
|
||
|
||
const fs = require('fs')
|
||
const path = require('path')
|
||
const { loadManifest, ACCOUNTS_DIR } = require('./pipeline-utils')
|
||
|
||
function validateManifest(manifestPath) {
|
||
const issues = []
|
||
|
||
if (!fs.existsSync(manifestPath)) {
|
||
console.error(`错误: manifest 不存在: ${manifestPath}`)
|
||
process.exit(1)
|
||
}
|
||
|
||
let manifest
|
||
try {
|
||
manifest = loadManifest(manifestPath)
|
||
} catch (e) {
|
||
console.error(`错误: JSON 解析失败: ${e.message}`)
|
||
process.exit(1)
|
||
}
|
||
|
||
if (!manifest.account) issues.push('缺少顶层 account')
|
||
if (!manifest.imageModel) issues.push('缺少顶层 imageModel(可选: gemini, mj)')
|
||
if (!manifest.format) issues.push('缺少顶层 format(如 9:16)')
|
||
if (!manifest.items || !Array.isArray(manifest.items)) issues.push('缺少顶层 items 数组')
|
||
if (!manifest.mode) issues.push('缺少顶层 mode(single 或 framePair)')
|
||
|
||
if (manifest.items && Array.isArray(manifest.items)) {
|
||
manifest.items.forEach((item, i) => {
|
||
const prefix = `items[${i}]`
|
||
if (!item.script && !item.text) issues.push(`${prefix} 缺少 script 或 text(中文文案)`)
|
||
if (!item.shotDesc) issues.push(`${prefix} 缺少 shotDesc(分镜描述)`)
|
||
if (!item.imagePrompt) issues.push(`${prefix} 缺少 imagePrompt`)
|
||
if (manifest.mode === 'framePair' && !item.lastFramePrompt) {
|
||
issues.push(`${prefix} 首尾帧模式缺少 lastFramePrompt(imagePrompt 作为第一帧)`)
|
||
}
|
||
if (item.status && !['pending', 'generating', 'done', 'failed'].includes(item.status)) {
|
||
issues.push(`${prefix} status 无效: ${item.status}`)
|
||
}
|
||
if (item.status === 'done' && !item.file && !item.video && !item.url) {
|
||
issues.push(`${prefix} status=done 但缺少 file/video/url(素材路径)`)
|
||
}
|
||
})
|
||
}
|
||
|
||
if (issues.length === 0) {
|
||
console.log(`✓ Manifest 校验通过: ${manifestPath}`)
|
||
console.log(` ${manifest.items?.length || 0} items, account=${manifest.account}, mode=${manifest.mode}`)
|
||
} else {
|
||
console.error(`✗ 发现 ${issues.length} 个问题:`)
|
||
issues.forEach(issue => console.error(` - ${issue}`))
|
||
process.exit(1)
|
||
}
|
||
}
|
||
|
||
function validateAccount(accountId) {
|
||
const issues = []
|
||
const accountDir = path.join(ACCOUNTS_DIR, accountId)
|
||
|
||
if (!fs.existsSync(accountDir)) { console.error(`错误: 账号不存在: ${accountDir}`); process.exit(1) }
|
||
|
||
const accountPath = path.join(accountDir, 'account.json')
|
||
if (!fs.existsSync(accountPath)) { console.error('错误: 缺少 account.json'); process.exit(1) }
|
||
|
||
let config
|
||
try { config = JSON.parse(fs.readFileSync(accountPath, 'utf-8')) }
|
||
catch (e) { console.error(`错误: JSON 解析失败: ${e.message}`); process.exit(1) }
|
||
|
||
if (config.id !== accountId) issues.push(`id 不匹配: json="${config.id}" vs 目录="${accountId}"`)
|
||
if (!config.name) issues.push('缺少 name')
|
||
if (!config.imageModel) issues.push('缺少 imageModel')
|
||
if (!config.defaultFormat) issues.push('缺少 defaultFormat')
|
||
|
||
// 检查 prompts 文件
|
||
const promptFiles = [
|
||
{ field: 'storyboardPrompt', label: '分镜' },
|
||
{ field: 'imageStylePrompt', label: '图片提示词' },
|
||
{ field: 'videoStylePrompt', label: '视频提示词' },
|
||
]
|
||
for (const { field, label } of promptFiles) {
|
||
const relPath = config[field]
|
||
if (!relPath) {
|
||
issues.push(`缺少 ${field}(prompts 路径)`)
|
||
} else {
|
||
const absPath = path.join(accountDir, relPath)
|
||
if (!fs.existsSync(absPath)) {
|
||
issues.push(`${label}文件不存在: ${relPath}`)
|
||
}
|
||
}
|
||
}
|
||
|
||
const refDir = path.join(accountDir, 'references')
|
||
const localRefs = fs.existsSync(refDir)
|
||
? fs.readdirSync(refDir).filter(f => /\.(png|jpg|jpeg|webp)$/i.test(f))
|
||
: []
|
||
const topRefs = config.references || []
|
||
if (localRefs.length === 0 && topRefs.length === 0) {
|
||
issues.push('无参考图(建议至少 1 张)')
|
||
}
|
||
for (const ref of topRefs) {
|
||
if (!ref.url) issues.push(`参考图 ${ref.file} 缺少 url(未上传 OSS)`)
|
||
}
|
||
|
||
if (issues.length === 0) {
|
||
console.log(`✓ 账号校验通过: ${accountId}`)
|
||
console.log(` ${config.name}, 模型: ${config.imageModel}+${config.videoModel || '(未指定)'}`)
|
||
console.log(` 参考图: ${localRefs.length} 本地, ${topRefs.length} 已上传`)
|
||
} else {
|
||
console.error(`✗ 发现 ${issues.length} 个问题:`)
|
||
issues.forEach(i => console.error(` - ${i}`))
|
||
process.exit(1)
|
||
}
|
||
}
|
||
|
||
module.exports = { validateManifest, validateAccount }
|