2026-04-30 21:18:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Command: init — 从 account.json + AI 创意内容生成规范 manifest.json
|
|
|
|
|
|
*
|
|
|
|
|
|
* 校验 items → 继承账号配置 → 创建输出目录 → 写出 manifest
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const fs = require('fs')
|
|
|
|
|
|
const path = require('path')
|
2026-05-02 00:14:40 +08:00
|
|
|
|
const { loadAccountConfig, saveManifest, ensureDir, slugify, ACCOUNTS_DIR, SKILLS_DIR } = require('./pipeline-utils')
|
2026-04-30 21:18:31 +08:00
|
|
|
|
|
|
|
|
|
|
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 <JSON> 或 --items-file <path>')
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!Array.isArray(rawItems) || rawItems.length === 0) {
|
|
|
|
|
|
console.error('错误: items 必须是非空数组')
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 校验必填字段
|
2026-05-02 00:14:40 +08:00
|
|
|
|
const requiredFields = ['shotDesc', 'script']
|
|
|
|
|
|
const optionalFields = ['imagePrompt', 'videoPrompt', 'lastFramePrompt']
|
2026-04-30 21:18:31 +08:00
|
|
|
|
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) {
|
2026-05-02 00:14:40 +08:00
|
|
|
|
delete item.lastFramePrompt // 首尾帧模式 Step 2-A 补充
|
2026-04-30 21:18:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 00:44:18 +08:00
|
|
|
|
// 从 account.json 继承参考图(顶层 references)
|
|
|
|
|
|
const accountRefs = accountConfig.references || []
|
|
|
|
|
|
const references = accountRefs.map(ref => {
|
2026-04-30 21:18:31 +08:00
|
|
|
|
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) => {
|
2026-05-02 00:14:40 +08:00
|
|
|
|
const slug = slugify(raw.shotDesc || raw.script || `scene_${i + 1}`)
|
2026-04-30 21:18:31 +08:00
|
|
|
|
const item = {
|
|
|
|
|
|
id: i + 1,
|
|
|
|
|
|
status: 'pending',
|
2026-05-02 00:14:40 +08:00
|
|
|
|
file: `images/scene_${String(i + 1).padStart(2, '0')}_${slug}.jpeg`,
|
2026-04-30 21:18:31 +08:00
|
|
|
|
shotDesc: raw.shotDesc || '',
|
2026-05-01 01:52:02 +08:00
|
|
|
|
script: raw.script || '',
|
2026-04-30 21:18:31 +08:00
|
|
|
|
duration: raw.duration || 5,
|
|
|
|
|
|
imagePrompt: raw.imagePrompt,
|
|
|
|
|
|
confirmed: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
if (raw.directorRef) item.directorRef = raw.directorRef
|
|
|
|
|
|
if (raw.videoPrompt) item.videoPrompt = raw.videoPrompt
|
2026-05-01 15:21:59 +08:00
|
|
|
|
if (raw.keyword) item.keyword = raw.keyword
|
2026-04-30 21:18:31 +08:00
|
|
|
|
if (resolvedMode === 'framePair') item.lastFramePrompt = raw.lastFramePrompt
|
|
|
|
|
|
return item
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 组装 manifest
|
|
|
|
|
|
const manifest = {
|
|
|
|
|
|
account: accountId,
|
2026-05-01 00:44:18 +08:00
|
|
|
|
imageModel: options.imageModel || accountConfig.imageModel || 'gemini',
|
|
|
|
|
|
videoModel: options.videoModel || accountConfig.videoModel || 'veo3-fast-frames',
|
|
|
|
|
|
format: options.format || accountConfig.defaultFormat || '9:16',
|
2026-04-30 21:18:31 +08:00
|
|
|
|
mode: resolvedMode,
|
|
|
|
|
|
references,
|
2026-05-01 00:44:18 +08:00
|
|
|
|
...(accountConfig.ttsVoice ? { ttsVoice: accountConfig.ttsVoice } : {}),
|
|
|
|
|
|
...(accountConfig.ttsInstruction ? { ttsInstruction: accountConfig.ttsInstruction } : {}),
|
2026-05-01 14:16:08 +08:00
|
|
|
|
...(accountConfig.ttsRate ? { ttsRate: accountConfig.ttsRate } : {}),
|
2026-04-30 21:18:31 +08:00
|
|
|
|
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}`)
|
2026-05-02 00:14:40 +08:00
|
|
|
|
if (items.some(it => !it.imagePrompt)) {
|
|
|
|
|
|
console.log(` ⚠ ${items.filter(it => !it.imagePrompt).length} 个 item 缺少 imagePrompt,请运行 Step 2-A(图片提示词)补充`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (resolvedMode === 'framePair' && items.some(it => !it.lastFramePrompt)) {
|
|
|
|
|
|
console.log(` ⚠ ${items.filter(it => !it.lastFramePrompt).length} 个 item 缺少 lastFramePrompt,请运行 Step 2-A 补充`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (items.some(it => !it.videoPrompt && resolvedMode !== 'framePair')) {
|
2026-04-30 21:18:31 +08:00
|
|
|
|
console.log(` ⚠ ${items.filter(it => !it.videoPrompt).length} 个 item 缺少 videoPrompt,生视频阶段将跳过`)
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log()
|
|
|
|
|
|
|
|
|
|
|
|
return manifestPath
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = { initManifest }
|