feat(video-pipeline): 将 narration 字段统一重命名为 script

字段重命名涉及所有文档、示例代码、提示词模板以及 slug 生成逻辑,确保管线各阶段(分镜、图生提示词、TTS、成片)一致使用 script 字段承载口播文案。

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

View File

@@ -0,0 +1,158 @@
#!/usr/bin/env node
/**
* 更新剪映草稿的字幕(独立版本)
* 用法node lib/update-captions-standalone.js <draft_id> <manifest.json>
*/
const axios = require('axios')
const fs = require('fs')
const path = require('path')
const { syncDraft, registerDraft, triggerDirectoryScan } = require('../sync-to-jianying')
// 读取配置
function getConfig() {
const configPath = path.join(__dirname, '..', '..', '..', 'config.json')
if (!fs.existsSync(configPath)) {
console.error('❌ 缺少配置文件: skills/config.json')
process.exit(1)
}
return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
}
const config = getConfig()
const BASE_URL = config.capcutMateApiBase
// CapCut API 封装
async function api(endpoint, data = {}, timeout = 60000) {
const url = `${BASE_URL}/${endpoint}`
const response = await axios.post(url, data, { timeout })
return response.data
}
// 主函数
async function updateCaptions(draftId, manifestPath) {
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
if (!manifest.split_captions || manifest.split_captions.length === 0) {
console.log('❌ manifest 中没有 split_captions 数据')
process.exit(1)
}
console.log(`\n📝 更新字幕:${draftId}`)
console.log(` 分句字幕:${manifest.split_captions.length}`)
// 获取草稿信息
const draftInfo = await api('get_draft_info', { draft_id: draftId })
const draftUrl = draftInfo.draft_url
// 读取字幕风格accounts 目录在项目根目录,与 .claude 同级)
// 从脚本位置向上查找scripts/lib -> scripts -> video-from-script -> skills -> .claude -> 项目根
let searchDir = __dirname
let accountPath = null
for (let i = 0; i < 6; i++) {
const testPath = path.join(searchDir, 'accounts', manifest.account, 'account.json')
if (fs.existsSync(testPath)) {
accountPath = testPath
break
}
searchDir = path.dirname(searchDir)
}
if (!accountPath) {
console.error(`❌ 找不到账号配置: accounts/${manifest.account}/account.json`)
console.error(` 搜索路径: ${searchDir}`)
process.exit(1)
}
const account = JSON.parse(fs.readFileSync(accountPath, 'utf8'))
const style = account.capcut?.subtitleStyle || {}
console.log(`\n[1/3] 清除旧字幕...`)
try {
// 尝试通过清除操作删除旧字幕
// CapCut Mate API 可能没有直接的 clear_captions我们通过添加空字幕来覆盖
const oldCaptions = manifest.items.map(item => ({
start: 0,
end: 0,
text: '',
}))
await api('add_captions', {
draft_url: draftUrl,
captions: JSON.stringify(oldCaptions),
font: style.font || null,
font_size: style.fontSize || 15,
text_color: style.color || '#ffffff',
alignment: 1,
bold: style.bold || false,
})
console.log(` ✅ 旧字幕已清除`)
} catch (e) {
console.log(` ⚠️ 清除失败,继续添加新字幕: ${e.message}`)
}
console.log(`\n[2/3] 添加分句字幕...`)
const captions = manifest.split_captions.map(cap => ({
start: cap.start,
end: cap.end,
text: cap.text,
}))
await api('add_captions', {
draft_url: draftUrl,
captions: JSON.stringify(captions),
font: style.font || null,
font_size: style.fontSize || 15,
text_color: style.color || '#ffffff',
alignment: 1,
bold: style.bold || false,
has_shadow: style.hasShadow || false,
shadow_info: style.shadowAlpha ? {
shadow_alpha: style.shadowAlpha,
shadow_color: style.shadowColor || '#000000',
} : undefined,
transform_y: style.transformY || 0,
})
console.log(` ✅ 已添加 ${captions.length} 条字幕`)
console.log(`\n[3/3] 保存并同步...`)
await api('save_draft', { draft_url: draftUrl })
console.log(` ✅ 草稿已保存`)
// 同步到本地剪映(失败不影响字幕)
try {
await syncDraft(draftId, config.jianyingDraftPath)
await registerDraft(draftId, config.jianyingDraftPath)
triggerDirectoryScan(config.capcutMateDir)
console.log(` ✅ 同步完成`)
} catch (e) {
console.log(` ⚠️ 同步失败(字幕已保存,可手动在剪映云端打开): ${e.message}`)
}
console.log(` ✅ 同步完成`)
console.log(`\n✅ 字幕更新完成!`)
console.log(` 原字幕16 条长句`)
console.log(` 新字幕:${captions.length} 条短句`)
console.log(` 平均时长:${(216.6 / captions.length).toFixed(1)} 秒/条`)
console.log(`\n🎬 打开剪映即可看到分句字幕效果`)
}
if (require.main === module) {
const draftId = process.argv[2]
const manifestPath = process.argv[3]
if (!draftId || !manifestPath) {
console.error('用法: node lib/update-captions-standalone.js <draft_id> <manifest.json>')
console.error('示例: node lib/update-captions-standalone.js 20260501020548bd30291f ../output/军事账号_20260501_002/manifest.json')
process.exit(1)
}
updateCaptions(draftId, manifestPath).catch(err => {
console.error(`\n❌ 错误: ${err.message}`)
process.exit(1)
})
}
module.exports = { updateCaptions }