Files
video-create/run_batch_7videos.js
lc 1e110219ff feat: MiniMax TTS集成、口播批量剪辑流水线、执黑先行二号账号
- 新增 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>
2026-05-24 20:05:56 +08:00

242 lines
9.7 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
/**
* 执黑先行二号-风格延伸 — 7个完整视频批量生产
* 视频8/9/12/13/14/15/16
* 各视频对应不同视觉风格和音色
*/
const fs = require('fs')
const path = require('path')
const { spawnSync } = require('child_process')
const ACCOUNT = '执黑先行二号-风格延伸'
const SCRIPTS_DIR = path.join(__dirname, '.claude/skills/video-from-script/scripts')
const OUTPUT_BASE = path.join(__dirname, 'output')
// ============================================================================
// 7个视频配置
// ============================================================================
const VIDEOS = [
{
id: 'v8', scriptNum: 8, style: '硬核线条', voice: '不羁青年', topicB: '搞女人',
refs: [
'/Users/lc/Pictures/参考图片/硬核线条/微信图片_20260522223358_119_18.png',
'/Users/lc/Pictures/参考图片/硬核线条/微信图片_20260522223358_120_18.png',
'/Users/lc/Pictures/参考图片/硬核线条/微信图片_20260522223358_121_18.png'
],
storyboardFile: '/tmp/sb_v8.json'
},
{
id: 'v9', scriptNum: 9, style: '梦核水池', voice: '不羁青年', topicB: '没人懂你',
refs: [
'/Users/lc/Pictures/参考图片/梦核水池/微信图片_20260522230621_239_18.png',
'/Users/lc/Pictures/参考图片/梦核水池/微信图片_20260522230621_245_18.png',
'/Users/lc/Pictures/参考图片/梦核水池/微信图片_20260522232446_266_18.png'
],
storyboardFile: '/tmp/sb_v9.json'
},
{
id: 'v12', scriptNum: 12, style: '水墨', voice: '温暖少女', topicB: '看人看结果',
refs: [
'/Users/lc/Pictures/参考图片/水墨/5bbcc329-dd8e-4b84-b901-97daa00943c9.png'
],
storyboardFile: '/tmp/sb_v12.json'
},
{
id: 'v13', scriptNum: 13, style: '梦核人物', voice: '温暖少女', topicB: '内耗三层',
refs: [
'/Users/lc/Pictures/参考图片/梦核人物/微信图片_20260522232203_259_18.png',
'/Users/lc/Pictures/参考图片/梦核人物/微信图片_20260522232204_260_18.png',
'/Users/lc/Pictures/参考图片/梦核人物/微信图片_20260522232204_261_18.png'
],
storyboardFile: '/tmp/sb_v13.json'
},
{
id: 'v14', scriptNum: 14, style: '梦核怪异城市', voice: '不羁青年', topicB: '地狱相',
refs: [
'/Users/lc/Pictures/参考图片/梦核怪异城市/微信图片_20260522231537_252_18.png',
'/Users/lc/Pictures/参考图片/梦核怪异城市/微信图片_20260522231537_253_18.png',
'/Users/lc/Pictures/参考图片/梦核怪异城市/微信图片_20260522231537_254_18.png'
],
storyboardFile: '/tmp/sb_v14.json'
},
{
id: 'v15', scriptNum: 15, style: '梦核花池', voice: '温暖少女', topicB: '孩子结账',
refs: [
'/Users/lc/Pictures/参考图片/梦核花池/微信图片_20260522231256_246_18.png',
'/Users/lc/Pictures/参考图片/梦核花池/微信图片_20260522231256_247_18.png',
'/Users/lc/Pictures/参考图片/梦核花池/微信图片_20260522231256_248_18.png'
],
storyboardFile: '/tmp/sb_v15.json'
},
{
id: 'v16', scriptNum: 16, style: '剪纸报', voice: '不羁青年', topicB: '借鸡生蛋',
refs: [
'/Users/lc/Pictures/参考图片/剪纸报/7b730552-efb6-426a-ae69-d2c8d396fe16.png'
],
storyboardFile: '/tmp/sb_v16.json'
}
]
// ============================================================================
// 主流程
// ============================================================================
async function main() {
console.log(`\n${'='.repeat(60)}`)
console.log(`执黑先行二号-风格延伸 — 7个视频批量生产`)
console.log(`${'='.repeat(60)}\n`)
const results = []
const today = new Date()
const dateStr = `${today.getMonth()+1}${String(today.getDate()).padStart(2,'0')}`
for (let i = 0; i < VIDEOS.length; i++) {
const v = VIDEOS[i]
const jobNum = String(i + 1).padStart(2, '0')
console.log(`\n[${'='.repeat(55)}]`)
console.log(`[${jobNum}/07] ${v.style} | ${v.voice} | 脚本${v.scriptNum}`)
console.log(`[${'='.repeat(55)}]`)
// 1. 检查分镜文件
if (!fs.existsSync(v.storyboardFile)) {
console.error(` ❌ 分镜文件不存在: ${v.storyboardFile}`)
results.push({ id: v.id, style: v.style, status: 'failed', error: '分镜文件缺失' })
continue
}
const items = JSON.parse(fs.readFileSync(v.storyboardFile, 'utf-8'))
console.log(` 分镜: ${items.length} 个 shot`)
// 2. 过滤掉 duration > 6 的 shot安全起见
const validItems = items.map(it => {
const d = Number(it.duration) || 5
return { ...it, duration: Math.min(d, 5.9) }
})
// 3. 写临时 items 文件
const tmpItems = `/tmp/items_${v.id}_${Date.now()}.json`
fs.writeFileSync(tmpItems, JSON.stringify(validItems), 'utf-8')
// 4. pipeline.js init
console.log(` → init manifest...`)
const initResult = spawnSync('node', [
path.join(SCRIPTS_DIR, 'pipeline.js'), 'init',
'--account', ACCOUNT,
'--mode', 'single',
'--items-file', tmpItems,
'--image-model', 'gpt-image',
'--video-model', 'kling',
'--tts-voice', v.voice,
'--format', '9:16'
], { cwd: __dirname, encoding: 'utf-8', timeout: 60000, stdio: 'pipe' })
if (initResult.status !== 0) {
console.error(` ❌ init 失败:`, initResult.stderr.slice(0, 300))
results.push({ id: v.id, style: v.style, status: 'failed', error: 'init failed: ' + initResult.stderr.slice(0,200) })
try { fs.unlinkSync(tmpItems) } catch {}
continue
}
// 从 init 输出中提取 manifest 路径
const initOut = initResult.stdout + initResult.stderr
const manifestMatch = initOut.match(/Manifest 已创建: (.+\.json)/)
if (!manifestMatch) {
console.error(' ❌ 无法提取 manifest 路径')
console.log(' init stdout:', initOut.slice(0, 500))
results.push({ id: v.id, style: v.style, status: 'failed', error: 'no manifest path' })
try { fs.unlinkSync(tmpItems) } catch {}
continue
}
const manifestPath = manifestMatch[1].trim()
console.log(` ✅ manifest: ${path.basename(path.dirname(manifestPath))}`)
try { fs.unlinkSync(tmpItems) } catch {}
// 5. 更新 manifest: 加 ttsEngine + references
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
manifest.ttsEngine = 'minimax'
manifest.ttsRate = 1.15
manifest.references = v.refs.map(f => ({ file: f }))
// 确保 items 里的 videoPrompt 字段存在pipeline 需要)
manifest.items.forEach((item, idx) => {
if (!item.videoPrompt && validItems[idx]?.videoPrompt) {
item.videoPrompt = validItems[idx].videoPrompt
}
})
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8')
console.log(` ✅ manifest 更新完成 (ttsEngine=minimax, refs=${v.refs.length})`)
// 6. 运行 pipeline: tts → images → upload → videos → assemble
console.log(` → 运行 pipeline (预计30-60分钟)...`)
const runResult = spawnSync('node', [
path.join(SCRIPTS_DIR, 'pipeline.js'), 'run',
'--manifest', manifestPath,
'--phase', 'tts,images,upload,videos,assemble'
], {
cwd: __dirname,
encoding: 'utf-8',
timeout: 3600000, // 1小时
stdio: 'inherit'
})
// 7. 读取结果
const finalManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
const draftUrl = finalManifest.draftUrl || ''
const ok = runResult.status === 0 && !!draftUrl
if (ok) {
// 8. 重命名草稿
const draftId = draftUrl.split('draft_id=')[1] || ''
if (draftId) {
const draftBase = '/Users/lc/Movies/JianyingPro/User Data/Projects/com.lveditor.draft'
const oldPath = path.join(draftBase, draftId)
const newName = `执黑先行二号-风格延伸_${dateStr}_${jobNum}_${v.topicB}`
const newPath = path.join(draftBase, newName)
if (fs.existsSync(oldPath)) {
try {
fs.renameSync(oldPath, newPath)
console.log(` ✅ 草稿已重命名: ${newName}`)
} catch (e) {
console.log(` ⚠ 草稿重命名失败: ${e.message}`)
}
}
}
console.log(` ✅ 完成 | draftUrl: ${draftUrl}`)
results.push({ id: v.id, scriptNum: v.scriptNum, style: v.style, voice: v.voice,
status: 'done', draftUrl, draftName: `执黑先行二号-风格延伸_${dateStr}_${jobNum}_${v.topicB}`,
shots: validItems.length })
} else {
console.log(` ❌ 失败 (exit ${runResult.status})`)
results.push({ id: v.id, scriptNum: v.scriptNum, style: v.style, voice: v.voice,
status: 'failed', shots: validItems.length })
}
}
// ====== 最终汇总 ======
console.log(`\n${'='.repeat(60)}`)
console.log('批量生产完成 — 最终结果表')
console.log(`${'='.repeat(60)}\n`)
console.log('| # | 脚本 | 风格 | 音色 | Shot数 | 状态 | 草稿名称 | draftUrl |')
console.log('|---|------|------|------|-------|------|---------|---------|')
results.forEach((r, i) => {
const num = String(i+1).padStart(2,'0')
const status = r.status === 'done' ? '✅' : '❌'
const draftName = r.draftName || '-'
const url = r.draftUrl || '-'
console.log(`| ${num} | ${r.scriptNum||'-'} | ${r.style} | ${r.voice||'-'} | ${r.shots||'-'} | ${status} | ${draftName} | ${url} |`)
})
// 保存结果
const resultPath = path.join(OUTPUT_BASE, `执黑先行二号-风格延伸_${new Date().toISOString().slice(0,10)}_7视频批量结果.json`)
fs.writeFileSync(resultPath, JSON.stringify(results, null, 2), 'utf-8')
console.log(`\n结果已保存: ${resultPath}`)
}
main().catch(err => {
console.error('脚本执行失败:', err.message)
process.exit(1)
})