- 新增 minimax-tts.js 和 minimax-voice-clone.js 脚本 - 新增口播批量剪辑流水线 (口播_assemble.js, 口播_pipeline.js) - 更新 video-from-script 各阶段脚本 (kling, images, tts, videos) - 新增执黑先行二号-风格延伸账号 - 更新执黑先行 account.json 配置 - 替换 ugc_product_seeding 参考图 - 更新 CLAUDE.md 和依赖配置 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
162 lines
6.7 KiB
JavaScript
162 lines
6.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 修复 confirmed=false 导致 0 视频的问题
|
|
* 对所有 7 个视频:补跑 videos → upload → assemble
|
|
* 视频阶段 2 并发,装配串行
|
|
*/
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
const { spawn, spawnSync } = require('child_process')
|
|
|
|
const SCRIPTS = path.join(__dirname, '.claude/skills/video-from-script/scripts/pipeline.js')
|
|
const DRAFT_BASE = '/Users/lc/Movies/JianyingPro/User Data/Projects/com.lveditor.draft'
|
|
const BASE = path.join(__dirname, 'output')
|
|
|
|
const MANIFESTS = [
|
|
{ mid: '011', id:'v8', scriptNum:8, style:'硬核线条', voice:'不羁青年', topicB:'搞女人', seq:18, needVideos:true },
|
|
{ mid: '012', id:'v9', scriptNum:9, style:'梦核水池', voice:'不羁青年', topicB:'没人懂你', seq:19, needVideos:true },
|
|
{ mid: '013', id:'v12', scriptNum:12, style:'水墨', voice:'温暖少女', topicB:'看人看结果', seq:20, needVideos:true },
|
|
{ mid: '019', id:'v13', scriptNum:13, style:'梦核人物', voice:'温暖少女', topicB:'内耗三层', seq:21, needVideos:true },
|
|
{ mid: '020', id:'v14', scriptNum:14, style:'梦核怪异城市', voice:'不羁青年', topicB:'地狱相', seq:22, needVideos:true },
|
|
{ mid: '021', id:'v15', scriptNum:15, style:'梦核花池', voice:'温暖少女', topicB:'孩子结账', seq:23, needVideos:false },
|
|
{ mid: '022', id:'v16', scriptNum:16, style:'剪纸报', voice:'不羁青年', topicB:'借鸡生蛋', seq:24, needVideos:true, needUpload:true },
|
|
]
|
|
|
|
function log(fd, msg) { fs.writeSync(fd, `[${new Date().toISOString().slice(11,19)}] ${msg}\n`) }
|
|
|
|
function runPipeline(manifestPath, phase, logFd, timeout) {
|
|
return new Promise((resolve) => {
|
|
const child = spawn('node', [SCRIPTS, 'run', '--manifest', manifestPath, '--phase', phase], {
|
|
cwd: __dirname, stdio: ['ignore', logFd, logFd], timeout: timeout || 3600000
|
|
})
|
|
child.on('close', code => resolve(code))
|
|
child.on('error', err => { log(logFd, `spawn error: ${err.message}`); resolve(1) })
|
|
})
|
|
}
|
|
|
|
async function runParallel(items, fn, limit) {
|
|
const results = [], inflight = new Set()
|
|
for (const item of items) {
|
|
const p = fn(item).then(r => { inflight.delete(p); return r })
|
|
inflight.add(p); results.push(p)
|
|
if (inflight.size >= limit) await Promise.race(inflight)
|
|
}
|
|
return Promise.all(results)
|
|
}
|
|
|
|
async function main() {
|
|
const logDir = `/tmp/batch_fix_${Date.now()}`
|
|
fs.mkdirSync(logDir, { recursive: true })
|
|
const start = Date.now()
|
|
|
|
console.log(`${'='.repeat(60)}`)
|
|
console.log(`修复视频 + 装配 — 7个视频`)
|
|
console.log(`${'='.repeat(60)}\n`)
|
|
|
|
// ==== Step 0: 修复 022 的 upload ====
|
|
const v22 = MANIFESTS.find(m => m.mid === '022')
|
|
if (v22.needUpload) {
|
|
const mf = path.join(BASE, `执黑先行二号-风格延伸_20260523_022/manifest.json`)
|
|
console.log('[Step 0] 修复 v16 upload 阶段...')
|
|
const code = spawnSync('node', [SCRIPTS, 'run', '--manifest', mf, '--phase', 'upload'], {
|
|
cwd: __dirname, encoding: 'utf-8', timeout: 120000, stdio: 'inherit'
|
|
})
|
|
console.log(code.status === 0 ? ' ✅ upload 完成' : ' ⚠ upload 退出码: ' + code.status)
|
|
}
|
|
|
|
// ==== Step 1: 并行 videos ====
|
|
const needVids = MANIFESTS.filter(m => m.needVideos)
|
|
console.log(`\n[Step 1] 视频生成 (${needVids.length}个, 2并发)\n`)
|
|
|
|
let vidDone = 0
|
|
await runParallel(needVids, async (v) => {
|
|
const mf = path.join(BASE, `执黑先行二号-风格延伸_20260523_${v.mid}/manifest.json`)
|
|
const lf = fs.openSync(path.join(logDir, `vid_${v.mid}_${v.topicB}.log`), 'w')
|
|
log(lf, `开始视频生成: ${v.style}`)
|
|
|
|
// 清除旧 draft 防止冲突
|
|
const m = JSON.parse(fs.readFileSync(mf, 'utf-8'))
|
|
delete m.draftUrl
|
|
// 重设备注阶段状态
|
|
if (m.pipeline?.phases?.assemble) m.pipeline.phases.assemble = 'pending'
|
|
if (m.pipeline?.phases?.videos) m.pipeline.phases.videos = 'pending'
|
|
fs.writeFileSync(mf, JSON.stringify(m, null, 2), 'utf-8')
|
|
|
|
const code = await runPipeline(mf, 'videos', lf, 3600000)
|
|
|
|
// 检查结果
|
|
const m2 = JSON.parse(fs.readFileSync(mf, 'utf-8'))
|
|
const vidCount = m2.items.filter(it => it.video || it.videoUrl).length
|
|
log(lf, `视频生成完成: ${vidCount}/${m2.items.length} items 有视频 (exit ${code})`)
|
|
fs.closeSync(lf)
|
|
|
|
vidDone++
|
|
console.log(` [视频 ${vidDone}/${needVids.length}] ${code===0?'✅':'⚠️'} ${v.style} | ${vidCount}个视频`)
|
|
return { ...v, vidCode: code, vidCount }
|
|
}, 2)
|
|
|
|
// ==== Step 2: 串行 assemble ====
|
|
console.log(`\n[Step 2] 装配 (7个, 串行)\n`)
|
|
|
|
let asmDone = 0
|
|
for (const v of MANIFESTS) {
|
|
const mf = path.join(BASE, `执黑先行二号-风格延伸_20260523_${v.mid}/manifest.json`)
|
|
const lf = fs.openSync(path.join(logDir, `asm_${v.mid}_${v.topicB}.log`), 'w')
|
|
log(lf, `开始装配: ${v.style}`)
|
|
|
|
const code = await runPipeline(mf, 'assemble', lf, 300000)
|
|
|
|
let draftUrl = '', draftName = ''
|
|
if (code === 0) {
|
|
const m = JSON.parse(fs.readFileSync(mf, 'utf-8'))
|
|
draftUrl = m.draftUrl || ''
|
|
}
|
|
|
|
if (draftUrl) {
|
|
const today = new Date()
|
|
const ds = `${today.getMonth()+1}${String(today.getDate()).padStart(2,'0')}`
|
|
draftName = `执黑先行二号-风格延伸_${ds}_${String(v.seq).padStart(2,'0')}_${v.topicB}`
|
|
|
|
const draftId = draftUrl.split('draft_id=')[1] || ''
|
|
if (draftId) {
|
|
const oldP = path.join(DRAFT_BASE, draftId)
|
|
const newP = path.join(DRAFT_BASE, draftName)
|
|
if (fs.existsSync(oldP)) {
|
|
try { fs.renameSync(oldP, newP); log(lf, `✅ 草稿已改名: ${draftName}`) }
|
|
catch(e) { log(lf, `⚠ 改名失败: ${e.message}`) }
|
|
}
|
|
// 删除旧草稿(如果存在)
|
|
const oldDraft = path.join(DRAFT_BASE, draftName)
|
|
// newP already set, skip duplicate check
|
|
}
|
|
log(lf, `✅ 装配完成 | ${draftUrl}`)
|
|
} else {
|
|
log(lf, `❌ 装配失败 (exit ${code})`)
|
|
}
|
|
fs.closeSync(lf)
|
|
|
|
asmDone++
|
|
const status = draftUrl ? '✅' : '❌'
|
|
console.log(` [装配 ${asmDone}/7] ${status} 脚本${v.scriptNum} ${v.style} | ${draftName || 'failed'}`)
|
|
|
|
v.result = { code, draftUrl, draftName }
|
|
}
|
|
|
|
// ==== Final Report ====
|
|
const total = Math.floor((Date.now() - start) / 60000)
|
|
console.log(`\n${'='.repeat(60)}`)
|
|
console.log(`全部完成 — ${total}min`)
|
|
console.log(`${'='.repeat(60)}`)
|
|
|
|
const done = MANIFESTS.filter(m => m.result?.draftUrl)
|
|
const failed = MANIFESTS.filter(m => !m.result?.draftUrl)
|
|
console.log(`✅ ${done.length} 成功 | ❌ ${failed.length} 失败\n`)
|
|
|
|
for (const v of MANIFESTS) {
|
|
const s = v.result?.draftUrl ? '✅' : '❌'
|
|
console.log(` ${s} 脚本${v.scriptNum} ${v.style} | ${v.result?.draftName || v.result?.code || 'N/A'}`)
|
|
}
|
|
}
|
|
|
|
main().catch(e => { console.error(e.message); process.exit(1) })
|