- 引入转场策略系统(`getTransition`),支持 `fixed`、`director`、`rhythm` 三种模式 - 根据账号配置文件动态读取转场配置(`loadTransitions`) - 图片和视频轨道分别调用转场策略,替代原有的固定“闪白”转场 - 支持 `byPosition`(hook/body/keypoint/closing)和 `byDirector` 两种高级选择策略 - 图片动画支持 `loop_animation` 与 `in_animation` 解析(“缩放”、“弹入”等组合) - TTS 合成新增 `rate` 字段(源自账号配置 `ttsRate`),默认语速调整为 1.1 - 默认动画类型从 `kenburns-zoom` 改为 `缩放`,适配中文 CapCut
140 lines
4.6 KiB
JavaScript
140 lines
4.6 KiB
JavaScript
/**
|
||
* 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 <JSON> 或 --items-file <path>')
|
||
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 } : {}),
|
||
...(accountConfig.ttsRate ? { ttsRate: accountConfig.ttsRate } : {}),
|
||
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 }
|