#!/usr/bin/env node /** * 更新剪映草稿的字幕(独立版本) * 用法:node lib/update-captions-standalone.js */ 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 ') 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 }