refactor(video-pipeline): 将 CapCut 组装脚本拆分为模块化架构
将 monolith 的 capcut_assemble.js 重构为核心编排器,提取基础设施层(capcut-api)、时间线算法(capcut-timeline)和轨道操作(capcut-tracks)为独立模块。此拆分使 Agent 未来对字幕风格、Ken Burns、转场、特效等调整只需关注 capcut-tracks.js,无需理解全流程编排逻辑。
This commit is contained in:
94
.claude/skills/video-from-script/scripts/lib/capcut-api.js
Normal file
94
.claude/skills/video-from-script/scripts/lib/capcut-api.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* CapCut API 基础设施层
|
||||
*
|
||||
* 提供: 配置加载、API 封装、CLI 解析、工具函数
|
||||
* 无业务逻辑,纯基础设施。
|
||||
*/
|
||||
|
||||
const axios = require('axios')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const { execFile } = require('child_process')
|
||||
|
||||
const US = 1_000_000
|
||||
|
||||
let _config = null
|
||||
function getConfig() {
|
||||
if (_config) return _config
|
||||
const configPath = path.join(__dirname, '..', '..', '..', 'config.json')
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.error('缺少配置文件: skills/config.json')
|
||||
console.error('请运行 node setup.js 生成配置')
|
||||
process.exit(1)
|
||||
}
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
||||
if (!config.jianyingDraftPath || !config.capcutMateDir || !config.capcutMateApiBase) {
|
||||
console.error('config.json 需要填写 jianyingDraftPath、capcutMateDir 和 capcutMateApiBase')
|
||||
process.exit(1)
|
||||
}
|
||||
_config = config
|
||||
return _config
|
||||
}
|
||||
|
||||
const BASE_URL = getConfig().capcutMateApiBase
|
||||
|
||||
async function api(endpoint, data = {}, timeout = 60000) {
|
||||
const url = `${BASE_URL}/${endpoint}`
|
||||
const method = endpoint === 'get_draft' ? 'get' : 'post'
|
||||
try {
|
||||
const res = method === 'get'
|
||||
? await axios.get(url, { params: data, timeout })
|
||||
: await axios.post(url, data, { timeout })
|
||||
if (res.data.code !== undefined && res.data.code !== 0) {
|
||||
throw new Error(`API [${endpoint}] 返回错误: ${res.data.message}`)
|
||||
}
|
||||
return res.data
|
||||
} catch (err) {
|
||||
if (err.response) {
|
||||
throw new Error(`API [${endpoint}] HTTP ${err.response.status}: ${JSON.stringify(err.response.data)}`)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {}
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
if (argv[i].startsWith('--')) {
|
||||
const key = argv[i].slice(2)
|
||||
const value = argv[i + 1]
|
||||
if (value && !value.startsWith('--')) {
|
||||
args[key] = value
|
||||
i++
|
||||
} else {
|
||||
args[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
function getResolution(format) {
|
||||
const map = {
|
||||
'9:16': { width: 1080, height: 1920 },
|
||||
'16:9': { width: 1920, height: 1080 },
|
||||
'1:1': { width: 1080, height: 1080 },
|
||||
'4:3': { width: 1440, height: 1080 },
|
||||
}
|
||||
return map[format] || map['9:16']
|
||||
}
|
||||
|
||||
function getAudioDurationSec(filePath) {
|
||||
return new Promise((resolve) => {
|
||||
execFile('ffprobe', [
|
||||
'-v', 'quiet', '-show_entries', 'format=duration',
|
||||
'-of', 'csv=p=0', filePath
|
||||
], (err, stdout) => {
|
||||
if (err) { resolve(null); return }
|
||||
const dur = parseFloat(stdout.trim())
|
||||
resolve(dur > 0 ? dur : null)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { US, getConfig, BASE_URL, api, parseArgs, getResolution, getAudioDurationSec }
|
||||
Reference in New Issue
Block a user