Files
video-create/.claude/skills/video-from-script/scripts/pipeline.js
sion123 86b9b7948d feat(video-from-script): 重构工作流为子Agent分步执行并新增提示词模板系统
将视频制作工作流拆分为独立子步骤:分镜 → 图片提示词 → 生图 → 视频提示词 → 生视频 → 成片,每步由子Agent独立执行。引入prompts/目录统一管理提示词模板(分镜.md、图片提示词.md、视频提示词.md),通过account.json的storyboardPrompt/imageStylePrompt/videoStylePrompt字段引用。

变更内容:
- 新增confirmed机制和pipeline.js confirm命令,生图后必须人工确认才能继续
- manifest schema改用shotDesc/narration/duration/directorRef替代旧字段
- 文件命名规则从keyword改为slug(从shotDesc/narration派生)
- 删除旧的storyboard-rules.md和prompt-rules.md
- pipeline.js脚本拆分为lib/目录下的独立模块(cmd-init/cmd-confirm/cmd-validate/phase-*)
- 新增cmd-create-account支持一键创建带prompts目录的账号
- capcut_assemble支持narration字段替代text作为字幕源
- 新增.gitclaude/settings.json权限配置
2026-04-30 21:18:31 +08:00

237 lines
8.5 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.
#!/usr/bin/env node
/**
* 统一视频生产流程编排器
*
* 串联: 生图 → 上传 → 生视频 → TTS → 成片
* manifest.json 是唯一数据源,支持断点续跑
*
* 模块结构:
* pipeline.js ← CLI 入口 + 编排逻辑(本文件)
* lib/pipeline-utils.js ← 共享工具函数 & 路径常量
* lib/phase-*.js ← 各阶段处理器
* lib/cmd-*.js ← 子命令实现
*
* 用法:
* node pipeline.js run --manifest ./output/batch/manifest.json --account military --resume
* node pipeline.js run --manifest ./output/batch/manifest.json --phase upload,videos
* node pipeline.js status --manifest ./output/batch/manifest.json
*/
const { loadManifest, saveManifest, loadAccountConfig, log } = require('./lib/pipeline-utils')
const { phaseImages } = require('./lib/phase-images')
const { phaseUpload } = require('./lib/phase-upload')
const { phaseVideos } = require('./lib/phase-videos')
const { phaseTts } = require('./lib/phase-tts')
const { phaseAssemble } = require('./lib/phase-assemble')
const { initManifest } = require('./lib/cmd-init')
const { validateManifest, validateAccount } = require('./lib/cmd-validate')
const { showStatus } = require('./lib/cmd-status')
const { confirmManifest } = require('./lib/cmd-confirm')
const { createAccount } = require('./lib/cmd-create-account')
// ============================================================================
// 阶段注册表
// ============================================================================
const ALL_PHASES = ['images', 'upload', 'videos', 'tts', 'assemble']
const PHASE_HANDLERS = {
images: phaseImages,
upload: phaseUpload,
videos: phaseVideos,
tts: phaseTts,
assemble: phaseAssemble,
}
// ============================================================================
// Pipeline 编排
// ============================================================================
async function runPipeline(manifestPath, options) {
const manifest = loadManifest(manifestPath)
// 加载账号配置
const accountId = manifest.account || options.account
let accountConfig = {}
if (accountId) {
accountConfig = loadAccountConfig(accountId)
log('pipeline', `账号: ${accountConfig.name || accountId}`)
}
const phases = options.phases || ALL_PHASES
if (!manifest.pipeline) {
manifest.pipeline = { phases: {} }
}
// --retry-failed: 重置失败 item 状态
if (options.retryFailed) {
applyRetryFailed(manifest, phases)
}
log('pipeline', `阶段: ${phases.join(' → ')}`)
for (const phase of phases) {
const handler = PHASE_HANDLERS[phase]
if (!handler) { log('pipeline', `未知阶段: ${phase}`); continue }
if (options.resume && manifest.pipeline.phases[phase] === 'done') {
log(phase, '已完成,跳过 (--resume)')
continue
}
manifest.pipeline.phases[phase] = 'running'
saveManifest(manifestPath, manifest)
try {
await handler(manifest, manifestPath, { ...options, accountConfig })
const failedCount = manifest.items.filter(it => it.status === 'failed').length
if (failedCount > 0) {
manifest.pipeline.phases[phase] = 'partial'
log(phase, `完成 (${failedCount} 个失败)`)
} else {
manifest.pipeline.phases[phase] = 'done'
log(phase, '完成')
}
} catch (err) {
manifest.pipeline.phases[phase] = 'failed'
log(phase, `阶段失败: ${err.message}`)
saveManifest(manifestPath, manifest)
throw err
}
saveManifest(manifestPath, manifest)
}
log('pipeline', '全部完成')
}
function applyRetryFailed(manifest, phases) {
let resetCount = 0
for (const item of manifest.items) {
if (item.status === 'failed' || item.status === 'partial') {
if (item.url && item.videoPrompt && !item.video) {
item.status = 'done'
item.error = ''
resetCount++
} else if (!item.url && item.imagePrompt) {
item.status = 'pending'
item.error = ''
resetCount++
}
}
}
if (phases.includes('videos')) {
if (manifest.items.some(it => it.status === 'done' && it.url && it.videoPrompt && !it.video)) {
manifest.pipeline.phases.videos = 'pending'
}
}
if (phases.includes('images')) {
if (manifest.items.some(it => !it.status || it.status === 'pending')) {
manifest.pipeline.phases.images = 'pending'
}
}
if (phases.includes('upload')) manifest.pipeline.phases.upload = 'pending'
if (phases.includes('tts')) manifest.pipeline.phases.tts = 'pending'
if (resetCount > 0) log('pipeline', `重置 ${resetCount} 个失败 item (--retry-failed)`)
}
// ============================================================================
// CLI 参数解析
// ============================================================================
function parseArgs(argv) {
const args = {}
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--manifest' && argv[i + 1]) args.manifest = argv[++i]
else if (argv[i] === '--account' && argv[i + 1]) args.account = argv[++i]
else if (argv[i] === '--phase' && argv[i + 1]) args.phases = argv[++i].split(',')
else if (argv[i] === '--resume') args.resume = true
else if (argv[i] === '--retry-failed') args.retryFailed = true
else if (argv[i] === '--mode' && argv[i + 1]) args.mode = argv[++i]
else if (argv[i] === '--items' && argv[i + 1]) args.items = argv[++i]
else if (argv[i] === '--items-file' && argv[i + 1]) args.itemsFile = argv[++i]
else if (argv[i] === '--id' && argv[i + 1]) args.id = argv[++i]
else if (argv[i] === '--name' && argv[i + 1]) args.name = argv[++i]
else if (argv[i] === '--desc' && argv[i + 1]) args.desc = argv[++i]
else if (argv[i] === '--format' && argv[i + 1]) args.format = argv[++i]
else if (argv[i] === '--image-model' && argv[i + 1]) args.imageModel = argv[++i]
else if (argv[i] === '--video-model' && argv[i + 1]) args.videoModel = argv[++i]
else if (argv[i] === '--references' && argv[i + 1]) args.references = argv[++i]
else if (argv[i] === '--style' && argv[i + 1]) args.style = argv[++i]
else if (argv[i] === '--all') args.all = true
else if (!args.command) args.command = argv[i]
}
return args
}
// ============================================================================
// CLI 入口
// ============================================================================
async function main() {
const args = parseArgs(process.argv.slice(2))
const command = args.command
if (command === 'init') {
initManifest(args)
return
}
if (command === 'validate') {
if (!args.manifest) { console.error('用法: pipeline.js validate --manifest <path>'); process.exit(1) }
validateManifest(args.manifest)
return
}
if (command === 'confirm') {
confirmManifest(args)
return
}
if (command === 'status') {
if (!args.manifest) { console.error('用法: pipeline.js status --manifest <path>'); process.exit(1) }
showStatus(args.manifest)
return
}
if (command === 'run') {
if (!args.manifest) { console.error('用法: pipeline.js run --manifest <path> [--account id] [--phase p1,p2] [--resume] [--retry-failed]'); process.exit(1) }
await runPipeline(args.manifest, args)
return
}
if (command === 'create-account') {
await createAccount(args)
return
}
if (command === 'validate-account') {
if (!args.account) { console.error('用法: pipeline.js validate-account --account <id>'); process.exit(1) }
validateAccount(args.account)
return
}
console.log('用法:')
console.log(' pipeline.js create-account --id <id> --name <名称> [--desc ...] [--references file1,file2]')
console.log(' pipeline.js validate-account --account <id>')
console.log(' pipeline.js init --account <id> --mode <single|framePair> --items <JSON> [--items-file <path>]')
console.log(' pipeline.js validate --manifest <path>')
console.log(' pipeline.js confirm --manifest <path> --all')
console.log(' pipeline.js run --manifest <path> [--account id] [--phase p1,p2] [--resume] [--retry-failed]')
console.log(' pipeline.js status --manifest <path>')
console.log('')
console.log('Manifest 路径约定: output/{account}_{date}_{NNN}/manifest.json同天自增序号')
console.log('阶段: images, upload, videos, tts, assemble')
}
if (require.main === module) {
main().catch(err => {
console.error(`\n错误: ${err.message}`)
process.exit(1)
})
}
module.exports = { runPipeline, showStatus, initManifest, validateManifest, createAccount, validateAccount }