Files
video-create/.claude/skills/video-from-script/scripts/lib/update-captions-standalone.js
sion123 96f00ce7fd feat(video-pipeline): 将 narration 字段统一重命名为 script
字段重命名涉及所有文档、示例代码、提示词模板以及 slug 生成逻辑,确保管线各阶段(分镜、图生提示词、TTS、成片)一致使用 script 字段承载口播文案。

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

159 lines
5.1 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
/**
* 更新剪映草稿的字幕(独立版本)
* 用法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 }