Files
video-create/.claude/skills/video-from-script/scripts/lib/cmd-init.js
sion123 0998fd6ae1 feat(video-pipeline): 重构视频流水线,优化成片时间线规则和状态管理
- 引入 manifest.json 作为唯一状态源,所有子 Agent 操作回写 manifest
- 重构 timebuilder 逻辑,支持四种视频适配策略(加速/裁剪/放缓/画面停顿)
- 统一 TTS 阶段输出结构,单句和多句均写入 segments[]
- 重写字幕和配音生成,基于 segments 精确时长实现音画同步
- 新增 confirm 命令支持按 id 范围确认,上传阶段分离图片和视频
- 添加中间产物写入 output/ 目录的约束,清理废弃配置参数
2026-05-02 00:14:40 +08:00

149 lines
5.2 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'))
} 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
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 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 }