Files
video-create/.claude/skills/video-from-script/scripts/lib/cmd-init.js
sion123 0e3f0f7d0f feat(video-pipeline): 增强参考图自动上传与视频生成重试机制
- 在 `init-manifest` 阶段添加输入文件清理日志和 WARNING 提示
- `getReferences` 改为异步并自动将本地参考图上传至 OSS,减少手动操作
- `phase-videos` 支持 `pending`/`failed` 状态 item 的自动重试,自动清理旧视频引用
- 优化 `phase-assemble` 中字幕与配音开关的逻辑,根据实际内容动态判断
2026-05-03 02:03:17 +08:00

155 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 (!item[f]) {
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 }