/** * Command: init — 从 account.json + AI 创意内容生成规范 manifest.json * * 校验 items → 继承账号配置 → 创建输出目录 → 写出 manifest */ const fs = require('fs') const path = require('path') const { loadAccountConfig, saveManifest, ensureDir, ACCOUNTS_DIR, SKILLS_DIR } = require('./pipeline-utils') function initManifest(options) { const { account: accountId, mode, items: itemsJson, itemsFile } = options if (!accountId) { console.error('错误: 必须指定 --account <账号ID>') process.exit(1) } const accountConfig = loadAccountConfig(accountId) // 解析 items let rawItems if (itemsFile) { const filePath = path.resolve(itemsFile) if (!fs.existsSync(filePath)) { console.error(`错误: items 文件不存在: ${filePath}`) process.exit(1) } rawItems = JSON.parse(fs.readFileSync(filePath, 'utf-8')) } else if (itemsJson) { rawItems = JSON.parse(itemsJson) } else { console.error('错误: 必须指定 --items 或 --items-file ') process.exit(1) } if (!Array.isArray(rawItems) || rawItems.length === 0) { console.error('错误: items 必须是非空数组') process.exit(1) } // 校验必填字段 const requiredFields = ['shotDesc', 'script', 'imagePrompt'] const resolvedMode = mode || 'single' for (let i = 0; i < rawItems.length; i++) { const item = rawItems[i] for (const f of requiredFields) { if (!item[f]) { console.error(`错误: items[${i}] 缺少必填字段 "${f}"`) process.exit(1) } } if (resolvedMode === 'framePair' && !item.lastFramePrompt) { console.error(`错误: 首尾帧模式 items[${i}] 缺少 "lastFramePrompt"(imagePrompt 作为第一帧)`) process.exit(1) } } // 从 account.json 继承参考图(顶层 references) const accountRefs = accountConfig.references || [] const references = accountRefs.map(ref => { const entry = {} if (ref.file) entry.file = path.join(ACCOUNTS_DIR, accountId, 'references', ref.file) if (ref.url) entry.url = ref.url return entry }) // 构建 items const items = rawItems.map((raw, i) => { const item = { id: i + 1, status: 'pending', shotDesc: raw.shotDesc || '', script: raw.script || '', duration: raw.duration || 5, imagePrompt: raw.imagePrompt, confirmed: false, } if (raw.directorRef) item.directorRef = raw.directorRef if (raw.videoPrompt) item.videoPrompt = raw.videoPrompt if (resolvedMode === 'framePair') item.lastFramePrompt = raw.lastFramePrompt return item }) // 组装 manifest const manifest = { account: accountId, imageModel: options.imageModel || accountConfig.imageModel || 'gemini', videoModel: options.videoModel || accountConfig.videoModel || 'veo3-fast-frames', format: options.format || accountConfig.defaultFormat || '9:16', mode: resolvedMode, references, ...(accountConfig.ttsVoice ? { ttsVoice: accountConfig.ttsVoice } : {}), ...(accountConfig.ttsInstruction ? { ttsInstruction: accountConfig.ttsInstruction } : {}), items, } // 创建输出目录(自增序号) const date = new Date() const dateStr = [ date.getFullYear(), String(date.getMonth() + 1).padStart(2, '0'), String(date.getDate()).padStart(2, '0'), ].join('') const prefix = `${accountConfig.name}_${dateStr}` const outputBase = path.join(SKILLS_DIR, '..', '..', '..', 'output') ensureDir(outputBase) let seq = 1 while (fs.existsSync(path.join(outputBase, `${prefix}_${String(seq).padStart(3, '0')}`))) { seq++ } const dirName = `${prefix}_${String(seq).padStart(3, '0')}` const outputDir = path.join(outputBase, dirName) ensureDir(outputDir) ensureDir(path.join(outputDir, 'images')) ensureDir(path.join(outputDir, 'videos')) ensureDir(path.join(outputDir, 'audio')) const manifestPath = path.join(outputDir, 'manifest.json') saveManifest(manifestPath, manifest) console.log(`\nManifest 已创建: ${manifestPath}`) console.log(` 账号: ${accountId} (${accountConfig.name})`) console.log(` 模型: ${manifest.imageModel} + ${manifest.videoModel}`) console.log(` 画幅: ${manifest.format}, 模式: ${manifest.mode}`) console.log(` Items: ${items.length}`) console.log(` 参考图: ${references.length}`) if (items.some(it => !it.videoPrompt)) { console.log(` ⚠ ${items.filter(it => !it.videoPrompt).length} 个 item 缺少 videoPrompt,生视频阶段将跳过`) } console.log() return manifestPath } module.exports = { initManifest }