159 lines
5.1 KiB
JavaScript
159 lines
5.1 KiB
JavaScript
|
|
#!/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 }
|