Files
video-create/.claude/skills/video-from-script/scripts/lib/cmd-init.js

155 lines
5.6 KiB
JavaScript
Raw Normal View History

/**
* Command: init account.json + AI 创意内容生成规范 manifest.json
*
* 校验 items 继承账号配置 创建输出目录 写出 manifest
*/
const fs = require('fs')
const path = require('path')
const { loadAccountConfig, saveManifest, ensureDir, slugify, 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'))
console.log(` 已读取并清理临时文件: ${path.basename(filePath)}`)
fs.unlinkSync(filePath)
} 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']
const optionalFields = ['imagePrompt', 'videoPrompt', 'lastFramePrompt']
const resolvedMode = mode || 'single'
for (let i = 0; i < rawItems.length; i++) {
const item = rawItems[i]
for (const f of requiredFields) {
if (!(f in item) || item[f] == null) {
console.error(`错误: items[${i}] 缺少必填字段 "${f}"`)
process.exit(1)
}
}
if (resolvedMode === 'framePair' && !item.lastFramePrompt) {
delete item.lastFramePrompt // 首尾帧模式 Step 2-A 补充
}
}
// 从 account.json 继承参考图(顶层 references仅使用带签名 OSS URL 的条目
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
})
const refsWithoutUrl = references.filter(r => !r.url)
if (refsWithoutUrl.length > 0) {
console.log(`${refsWithoutUrl.length} 个参考图缺少 OSS URLimages 阶段会自动上传`)
}
// 构建 items
const items = rawItems.map((raw, i) => {
const slug = slugify(raw.shotDesc || raw.script || `scene_${i + 1}`)
const item = {
id: i + 1,
status: 'pending',
file: `images/scene_${String(i + 1).padStart(2, '0')}_${slug}.jpeg`,
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 (raw.keyword) item.keyword = raw.keyword
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.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')) {
console.log(`${items.filter(it => !it.videoPrompt).length} 个 item 缺少 videoPrompt生视频阶段将跳过`)
}
console.log()
return manifestPath
}
module.exports = { initManifest }