- SKILL.md: 新增工作流阶段定义、质量卡点、分镜规则 - manifest-schema.md: 补充完整字段规范及类型定义 - phase-tts.js: 优化 TTS 合成长逻辑,添加进度追踪 - capcut-tracks.js: 扩展轨道构建能力,支持更多元素类型 - capcut-timeline.js: 改进时间线生成,支持淡入淡出 - capcut_assemble.js: 新增 assemble 阶段完整实现 - cmd-init.js: 完善 init 命令逻辑 - qwen-tts.js: 调整超时配置 - accounts/禁忌帝王学: 更新拆分/图像/台词提示词 - accounts/健身跟练: 新增账号含 account.json 及全套提示词模板 - 新增 workflow-issues-20260501.md 参考文档 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
248 lines
9.2 KiB
JavaScript
248 lines
9.2 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* 统一视频生产流程编排器
|
||
*
|
||
* 串联: 生图 → 上传 → 生视频 → TTS → 成片
|
||
* manifest.json 是唯一数据源,支持断点续跑
|
||
*
|
||
* 模块结构:
|
||
* pipeline.js ← CLI 入口 + 编排逻辑(本文件)
|
||
* lib/pipeline-utils.js ← 共享工具函数 & 路径常量
|
||
* lib/phase-*.js ← 各阶段处理器
|
||
* lib/cmd-*.js ← 子命令实现
|
||
*
|
||
* 用法:
|
||
* node scripts/pipeline.js run --manifest ./output/batch/manifest.json --account military --resume
|
||
* node scripts/pipeline.js run --manifest ./output/batch/manifest.json --phase upload,videos
|
||
* node scripts/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 = ['tts', 'images', 'upload', 'videos', 'assemble']
|
||
|
||
const PHASE_HANDLERS = {
|
||
tts: phaseTts,
|
||
images: phaseImages,
|
||
upload: phaseUpload,
|
||
videos: phaseVideos,
|
||
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) {
|
||
// 图片未上传 → 重试图片阶段
|
||
// 如果首帧已存在但 lastFrame 失败,只重置 lastFrame 相关
|
||
if (item.file && manifest.mode === 'framePair' && !item.lastFrame) {
|
||
item.status = 'done' // 保留首帧,只补 lastFrame
|
||
item.error = ''
|
||
resetCount++
|
||
} else {
|
||
item.status = 'pending'
|
||
item.error = ''
|
||
delete item.file // 清除旧文件引用,避免重复
|
||
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') || (it.status === 'done' && manifest.mode === 'framePair' && !it.lastFrame))) {
|
||
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] === '--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>] [--image-model gemini|gpt-image|mj] [--video-model veo3-fast|grok|kling] [--format 9:16]')
|
||
console.log(' pipeline.js validate --manifest <path>')
|
||
console.log(' pipeline.js confirm --manifest <path> --all')
|
||
console.log(' pipeline.js confirm --manifest <path> --items 1,3,5')
|
||
console.log(' pipeline.js run --manifest <path> --phase tts,images,upload,videos,assemble')
|
||
console.log(' pipeline.js run --manifest <path> --resume')
|
||
console.log(' pipeline.js status --manifest <path>')
|
||
console.log('')
|
||
console.log('Manifest 路径约定: output/{account}_{date}_{NNN}/manifest.json(同天自增序号)')
|
||
console.log('阶段: tts → images → upload → videos → assemble(TTS提前)')
|
||
}
|
||
|
||
if (require.main === module) {
|
||
main().catch(err => {
|
||
console.error(`\n错误: ${err.message}`)
|
||
process.exit(1)
|
||
})
|
||
}
|
||
|
||
module.exports = { runPipeline, showStatus, initManifest, validateManifest, createAccount, validateAccount }
|