feat(video-pipeline): 支持滤镜和转场从账号配置继承

- 新增 Q18 滤镜和 Q19 转场字段到账号创建参考文档
- 重构账号配置加载函数为通用 `loadAccountConfig`,支持读取滤镜和转场
- `capcut_assemble.js` 支持 CLI 参数优先、账号配置兜底的滤镜/特效继承逻辑
- 模板 `account.json` 将闭幕转场从 "黑场" 修正为 "闪黑
This commit is contained in:
2026-05-01 15:44:25 +08:00
parent e4723d9ce3
commit f2bc0df447
4 changed files with 71 additions and 38 deletions

View File

@@ -93,6 +93,8 @@ Phase 4: 技术配置(有默认值,可跳过)
| 16 | TTS 语气指令? | 无 | account.json 的 ttsInstruction描述期望的语气风格 |
| 17 | 关键字氛围词? | 开启(默认样式) | 画面中央大字叠加,增强冲击力。选项:关闭 / 默认样式 / 描述期望效果 |
| | 期望的花字风格?(选填) | 按账号风格推荐 | 根据视觉基调推荐,见下方花字推荐表 |
| 18 | 滤镜? | 无 | account.json 的 capcut.filter格式 `滤镜名:强度`(如 `质感电影:40`)。见下方滤镜推荐表 |
| 19 | 转场偏好? | 默认闪白 | account.json 的 capcut.transitions。见下方转场推荐表 |
**花字效果推荐表**92 种免费效果,按风格分类):
@@ -109,6 +111,33 @@ Phase 4: 技术配置(有默认值,可跳过)
> Agent 可调用 `get_text_effects` API 获取完整列表92 种免费 + 更多 VIP
> Q17 选"默认样式"时Agent 根据账号风格从上表自动匹配。
**滤镜推荐表**(免费滤镜,格式 `滤镜名:强度`,强度 0-100
| 账号风格 | 推荐滤镜 | 备选 |
|---------|---------|------|
| 暗黑/军事/权谋 | 暗调氛围:40 | 暗夜:50、质感暗调:35 |
| 电影/叙事 | 质感电影:40 | 情感电影:35、情绪电影:30 |
| 复古/怀旧 | 暗调复古电影:40 | 复古工业:45 |
| 科技/赛博 | 质感暗调:30 | — |
| 清新/生活 | 质感电影:25 | 情感电影:20 |
| 纪录/人文 | 质感电影:30 | 情绪电影:25 |
> 留空则不加滤镜。Agent 可调用 CapCut Mate API 获取完整滤镜列表。
**转场推荐表**(免费转场,`duration` 单位微秒,推荐 100000-300000
| 转场名 | 效果 | 适用场景 |
|--------|------|---------|
| 闪白 | 明闪过渡,节奏强 | 暗黑/军事/冲击类(默认推荐) |
| 闪白 II | 明闪变体,稍柔和 | 潮酷/街头 |
| 闪黑 | 暗闪过渡,压抑感 | 沉重/悬疑/结尾 |
| 溶解 | 画面缓慢交融 | 唯美/叙事/情感 |
| 叠化 | 经典淡入淡出 | 通用/纪录片 |
| 色彩溶解 | 带色彩过渡的溶解 | 艺术/创意 |
> 默认配置使用「闪白」+ 按位置变体hook 闪白、body 溶解、closing 闪黑),可直接采用默认。
> Agent 可调用 CapCut Mate API 获取完整转场列表。
**运动偏好 → 视频提示词映射**
| 用户选择 | 运动风格基调 |
@@ -150,6 +179,8 @@ Phase 4: 技术配置(有默认值,可跳过)
- TTS音色{Q15}
- TTS语气{Q16}
- 关键字氛围词:{Q17 开启/关闭,花字风格}
- 滤镜:{Q18}
- 转场:{Q19}
确认 "开始" → 创建账号
修改 → 调整后重新输出
@@ -178,6 +209,8 @@ Phase 4: 技术配置(有默认值,可跳过)
- 从 `_template/account.json` 复制骨架
- 填入 id、name、description、模型、画幅等
- Q17 选关闭时删除 `keywordStyle` 节;选自定义花字时更新 `textEffect` 字段
- Q18 填入 `capcut.filter`(格式 `滤镜名:强度`
- Q19 填入 `capcut.transitions` 配置
3. **生成分镜.md**
- 读取 `_template/prompts/通用分镜.md`

View File

@@ -233,6 +233,20 @@ async function assemble(args) {
}
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'))
// 从 account.json 自动继承 effects / filterCLI 参数优先)
let finalEffects = effectsStr
let finalFilter = filterStr
if (!finalEffects || !finalFilter) {
const accountData = loadAccountConfig(manifest)
if (!finalEffects && accountData.capcut?.effects?.length) {
finalEffects = accountData.capcut.effects.join(',')
}
if (!finalFilter && accountData.capcut?.filter) {
finalFilter = accountData.capcut.filter
}
}
const { width, height } = getResolution(format)
const defaultDurationUs = parseFloat(duration) * US
@@ -284,6 +298,8 @@ async function assemble(args) {
console.log(` 模式: ${mode} 画幅: ${format} (${width}x${height})`)
console.log(` 时间线: ${hasTTS ? 'TTS音频驱动' : `固定${duration}s/段`} 总时长: ${(totalDurationUs / US).toFixed(1)}s`)
console.log(` 字幕: ${subtitles} 配音: ${voiceover} 动画: ${animation}`)
if (finalEffects) console.log(` 特效: ${finalEffects}`)
if (finalFilter) console.log(` 滤镜: ${finalFilter}`)
console.log(` 素材: ${items.length} 个可用\n`)
const steps = []
@@ -397,7 +413,7 @@ async function assemble(args) {
}
}
}
if (changed) saveManifest(manifestFile, manifest)
if (changed) fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 2))
}
} catch (err) {
console.log(` OSS 上传失败,将尝试本地路径: ${err.message}\n`)
@@ -445,26 +461,26 @@ async function assemble(args) {
// -- 添加特效 --
step++; console.log(`[${step}/${totalSteps}] 添加特效...`)
if (effectsStr) {
if (finalEffects) {
try {
await addEffects(draftUrl, effectsStr, totalDurationUs)
await addEffects(draftUrl, finalEffects, totalDurationUs)
} catch (e) {
console.log(` 特效跳过: ${e.message}`)
}
} else {
console.log(' 跳过(未指定 --effects')
console.log(' 跳过(未配置特效')
}
// -- 添加滤镜 --
step++; console.log(`[${step}/${totalSteps}] 添加滤镜...`)
if (filterStr) {
if (finalFilter) {
try {
await addFilter(draftUrl, filterStr, totalDurationUs)
await addFilter(draftUrl, finalFilter, totalDurationUs)
} catch (e) {
console.log(` 滤镜跳过: ${e.message}`)
}
} else {
console.log(' 跳过(未指定 --filter')
console.log(' 跳过(未配置滤镜')
}
// -- 保存草稿 --
@@ -809,31 +825,23 @@ async function addBGM(draftUrl, bgmUrl, totalDurationUs) {
}
// ============================================================================
// 读取账号字幕风格配置
// 读取账号配置
// ============================================================================
function loadSubtitleStyle(manifest) {
function loadAccountConfig(manifest) {
const account = manifest.account
if (!account) return {}
const scriptDir = __dirname
const accountFile = path.join(scriptDir, '..', '..', '..', 'accounts', account, 'account.json')
const accountFile = path.join(__dirname, '..', '..', '..', '..', 'accounts', account, 'account.json')
if (!fs.existsSync(accountFile)) return {}
try {
const accountData = JSON.parse(fs.readFileSync(accountFile, 'utf-8'))
return accountData.capcut?.subtitleStyle || {}
} catch { return {} }
try { return JSON.parse(fs.readFileSync(accountFile, 'utf-8')) } catch { return {} }
}
function loadSubtitleStyle(manifest) {
return loadAccountConfig(manifest).capcut?.subtitleStyle || {}
}
function loadKeywordStyle(manifest) {
const account = manifest.account
if (!account) return {}
const scriptDir = __dirname
const accountFile = path.join(scriptDir, '..', '..', '..', 'accounts', account, 'account.json')
if (!fs.existsSync(accountFile)) return {}
try {
const accountData = JSON.parse(fs.readFileSync(accountFile, 'utf-8'))
return accountData.capcut?.keywordStyle || {}
} catch { return {} }
return loadAccountConfig(manifest).capcut?.keywordStyle || {}
}
// ============================================================================
@@ -894,15 +902,7 @@ async function addKeywordOverlays(draftUrl, items, timeline, style = {}) {
}
function loadTransitions(manifest) {
const account = manifest.account
if (!account) return null
const scriptDir = __dirname
const accountFile = path.join(scriptDir, '..', '..', '..', 'accounts', account, 'account.json')
if (!fs.existsSync(accountFile)) return null
try {
const accountData = JSON.parse(fs.readFileSync(accountFile, 'utf-8'))
return accountData.capcut?.transitions || null
} catch { return null }
return loadAccountConfig(manifest).capcut?.transitions || null
}
// ============================================================================

View File

@@ -40,12 +40,12 @@
"hook": { "name": "闪白", "duration": 100000 },
"keypoint": { "name": "闪白", "duration": 120000 },
"body": { "name": "溶解", "duration": 300000 },
"closing": { "name": "黑", "duration": 200000 }
"closing": { "name": "黑", "duration": 200000 }
},
"byDirector": {
"tarantino": { "name": "闪白", "duration": 100000 },
"kitano": { "name": "溶解", "duration": 400000 },
"fincher": { "name": "黑", "duration": 200000 }
"fincher": { "name": "黑", "duration": 200000 }
}
}
}

View File

@@ -21,7 +21,7 @@
"videoStylePrompt": "prompts/视频提示词.md",
"capcut": {
"effects": ["录制边框 III"],
"filter": "电影:40",
"filter": "质感电影:40",
"subtitleStyle": {
"font": "思源黑体 Heavy",
"fontSize": 24,
@@ -55,12 +55,12 @@
"hook": { "name": "闪白", "duration": 100000 },
"keypoint": { "name": "闪白", "duration": 120000 },
"body": { "name": "溶解", "duration": 300000 },
"closing": { "name": "黑", "duration": 200000 }
"closing": { "name": "黑", "duration": 200000 }
},
"byDirector": {
"tarantino": { "name": "闪白", "duration": 100000 },
"kitano": { "name": "溶解", "duration": 400000 },
"fincher": { "name": "黑", "duration": 200000 }
"fincher": { "name": "黑", "duration": 200000 }
}
}
}