#!/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) })