将 monolith 的 capcut_assemble.js 重构为核心编排器,提取基础设施层(capcut-api)、时间线算法(capcut-timeline)和轨道操作(capcut-tracks)为独立模块。此拆分使 Agent 未来对字幕风格、Ken Burns、转场、特效等调整只需关注 capcut-tracks.js,无需理解全流程编排逻辑。
95 lines
2.6 KiB
JavaScript
95 lines
2.6 KiB
JavaScript
/**
|
|
* 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 }
|