Files
video-create/.claude/skills/video-from-script/scripts/pipeline.js
sion123 96f00ce7fd feat(video-pipeline): 将 narration 字段统一重命名为 script
字段重命名涉及所有文档、示例代码、提示词模板以及 slug 生成逻辑,确保管线各阶段(分镜、图生提示词、TTS、成片)一致使用 script 字段承载口播文案。

同时增强 `capcut_assemble.js` 的配音与字幕模块:
- 配音支持本地路径模式(不上传 OSS),新增 `--split-captions` 参数启用分句字幕
- 分句字幕按标点切分长文本为短句,基于字数权重动态分配显示时长
- 新增 `update-captions-standalone.js` 独立工具,可在草稿已生成后单独更新字幕
2026-05-01 02:48:37 +08:00

237 lines
8.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.
#!/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 = ['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>] [--image-model gemini|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 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 }