- 新增 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>
242 lines
9.7 KiB
JavaScript
242 lines
9.7 KiB
JavaScript
#!/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)
|
||
})
|