feat(video-pipeline): 增强账号配置和分镜关键词支持

- 在 account.json 中新增 styles、ttsVoice、ttsInstruction、keywordStyle、transitions 等配置字段
- 分镜表 items 添加 keyword 字段,用于渲染关键词花字动画
- 重构 capcut_assemble.js 转场策略:前两段不加转场,rhythm 模式优化为只在关键位置添加
- 动画系统区分循环动画和入场动画,支持更丰富的动画组合(缩放、回弹伸缩等)
- 更新 SKILL.md 和 manifest-schema.md 文档以反映新增字段和流程变更
This commit is contained in:
2026-05-01 16:00:01 +08:00
parent f2bc0df447
commit 350f05fc3f
4 changed files with 76 additions and 33 deletions

View File

@@ -120,7 +120,7 @@ Step 1: 分镜脚本生成(子 Agent 执行)
- 主 Agent 将用户文案 + 模板交给子 Agent - 主 Agent 将用户文案 + 模板交给子 Agent
- 子 Agent 按模板要求输出分镜表 JSON - 子 Agent 按模板要求输出分镜表 JSON
```json ```json
[{"id":1,"shotDesc":"英文画面描述40-80词","script":"中文口播文案≤22字","duration":5,"directorRef":"tarantino"}] [{"id":1,"shotDesc":"英文画面描述40-80词","script":"中文口播文案≤22字","duration":5,"directorRef":"tarantino","keyword":"权力"}]
``` ```
- 主 Agent 审查分镜表时长合理、隐性动势完整、directorRef 已填) - 主 Agent 审查分镜表时长合理、隐性动势完整、directorRef 已填)
- 展示给用户确认,确认后进入 Step 2-A - 展示给用户确认,确认后进入 Step 2-A
@@ -136,7 +136,7 @@ Step 2-B: 生成静态分镜图 + Manifest 初始化
- 组装 items 并初始化 manifest**不含 videoPrompt** - 组装 items 并初始化 manifest**不含 videoPrompt**
```bash ```bash
node scripts/pipeline.js init --account <id> --mode <single|framePair> \ node scripts/pipeline.js init --account <id> --mode <single|framePair> \
--items '[{"shotDesc":"...","script":"...","duration":5,"imagePrompt":"...","directorRef":"tarantino"}]' --items '[{"shotDesc":"...","script":"...","duration":5,"imagePrompt":"...","directorRef":"tarantino","keyword":"权力"}]'
``` ```
- 脚本自动从 account.json 继承imageModel、videoModel、format、references - 脚本自动从 account.json 继承imageModel、videoModel、format、references
- 所有 item.confirmed = false - 所有 item.confirmed = false
@@ -183,7 +183,7 @@ node scripts/pipeline.js validate-account --account <id>
# 初始化 manifestStep 2-B 使用AI 只提供创意内容,不含 videoPrompt # 初始化 manifestStep 2-B 使用AI 只提供创意内容,不含 videoPrompt
node scripts/pipeline.js init --account <id> --mode <single|framePair> \ node scripts/pipeline.js init --account <id> --mode <single|framePair> \
--items '[{"shotDesc":"...","script":"...","duration":5,"imagePrompt":"...","directorRef":"tarantino"}]' --items '[{"shotDesc":"...","script":"...","duration":5,"imagePrompt":"...","directorRef":"tarantino","keyword":"权力"}]'
# 也可从文件读取 items适合大量数据 # 也可从文件读取 items适合大量数据
node scripts/pipeline.js init --account <id> --mode single --items-file ./items.json node scripts/pipeline.js init --account <id> --mode single --items-file ./items.json
@@ -354,8 +354,6 @@ output/{name}_{YYYYMMDD}_{NNN}/
--- ---
---
## 质量卡点Agent 可执行) ## 质量卡点Agent 可执行)
每个阶段完成后 Agent 自动校验不通过的自动修复。需要人工视觉判断的画质、动画、BGM等由用户在人工审查步骤处理。 每个阶段完成后 Agent 自动校验不通过的自动修复。需要人工视觉判断的画质、动画、BGM等由用户在人工审查步骤处理。
@@ -395,7 +393,7 @@ output/{name}_{YYYYMMDD}_{NNN}/
所有子技能共享以下资源(位于本目录): 所有子技能共享以下资源(位于本目录):
- `scripts/` — 共享脚本(gemini-image-generator.js, mj-image-generator.js, grok-video-generator.js, veo-video-generator.js, kling-video-generator.js, qwen-tts.js, capcut_assemble.js, sync-to-jianying.js, oss-upload.js - `scripts/` — 共享脚本(生图、生视频、TTS、成片组装、同步剪映、OSS 上传等
- `accounts/` — 账号配置(项目根目录,详见 [account-system.md](references/account-system.md) - `accounts/` — 账号配置(项目根目录,详见 [account-system.md](references/account-system.md)
- `references/account-system.md` — 账号系统说明 - `references/account-system.md` — 账号系统说明

View File

@@ -37,25 +37,62 @@ accounts/ # 项目根目录下
"id": "tech-talk", "id": "tech-talk",
"name": "科技解说", "name": "科技解说",
"description": "科技类短视频账号,深色背景,赛博朋克风格", "description": "科技类短视频账号,深色背景,赛博朋克风格",
"pipeline": "image-video",
"defaultFormat": "9:16",
"imageModel": "gemini", "imageModel": "gemini",
"videoModel": "kling", "videoModel": "veo3-fast",
"batchSize": 30, "batchSize": 30,
"styles": {
"赛博风格": {
"references": [
{ "file": "cyber_ref.png", "url": "https://oss.../cyber_ref.png" }
]
}
},
"ttsVoice": "cosyvoice-v3.5-plus-bailian-xxx",
"ttsInstruction": "用冷静理性的男性声音朗读,语速适中",
"storyboardPrompt": "prompts/分镜.md", "storyboardPrompt": "prompts/分镜.md",
"imageStylePrompt": "prompts/图片提示词.md", "imageStylePrompt": "prompts/图片提示词.md",
"videoStylePrompt": "prompts/视频提示词.md", "videoStylePrompt": "prompts/视频提示词.md",
"references": [ "references": [],
{ "file": "ref_001.png", "url": "https://oss.../ref_001.png" }
],
"capcut": { "capcut": {
"effects": ["录制边框 III"], "effects": ["录制边框 III"],
"filter": "电影感:40", "filter": "质感暗调:30",
"subtitleStyle": { "subtitleStyle": {
"fontSize": 36, "fontSize": 24,
"color": "#FFFFFF", "color": "#FFFFFF",
"highlightColor": "#FF6B35", "highlightColor": "#FF6B35",
"bold": true "bold": true,
"hasShadow": true,
"shadowColor": "#000000",
"shadowAlpha": 0.8,
"transformY": -380,
"alignment": 1,
"inAnimation": "淡入",
"outAnimation": "淡出"
}, },
"defaultBGM": "https://example.com/bgm_tech.mp3" "keywordStyle": {
"textEffect": "简约白色黑边花字",
"fontSize": 60,
"color": "#FFFFFF",
"bold": true,
"transformY": 0,
"inAnimation": "打字机效果",
"outAnimation": "模糊淡出",
"inAnimDuration": 300000,
"outAnimDuration": 300000
},
"transitions": {
"strategy": "rhythm",
"default": { "name": "闪白", "duration": 150000 },
"byPosition": {
"hook": { "name": "闪白", "duration": 100000 },
"keypoint": { "name": "闪白", "duration": 120000 },
"body": { "name": "溶解", "duration": 300000 },
"closing": { "name": "闪黑", "duration": 200000 }
}
},
"defaultBGM": ""
} }
} }
``` ```
@@ -65,17 +102,23 @@ accounts/ # 项目根目录下
| `id` | string | 账号唯一标识(与目录名一致) | | `id` | string | 账号唯一标识(与目录名一致) |
| `name` | string | 账号显示名 | | `name` | string | 账号显示名 |
| `description` | string | 一句话描述 | | `description` | string | 一句话描述 |
| `defaultFormat` | string | 默认画幅9:16 / 16:9 / 1:1 / 4:3 | | `pipeline` | string | 流水线类型:`image-video`(默认 |
| `imageModel` | string | 默认图片模型 | | `defaultFormat` | string | 默认画幅(`9:16` / `16:9` / `1:1` / `4:3` |
| `videoModel` | string | 默认视频模型 | | `imageModel` | string | 默认图片模型(`gemini` / `mj` |
| `videoModel` | string | 默认视频模型(`veo3-fast` / `grok-video-3` / `kling` |
| `batchSize` | number | 默认批量生成数量 | | `batchSize` | number | 默认批量生成数量 |
| `styles` | object | 命名风格预设,每项含 `references` 数组 |
| `ttsVoice` | string | TTS 音色 ID留空用 config.json 全局默认 |
| `ttsInstruction` | string | TTS 语气指令(描述期望的语气、语速、情感) |
| `storyboardPrompt` | string | 分镜提示词模板路径(相对于账号目录) | | `storyboardPrompt` | string | 分镜提示词模板路径(相对于账号目录) |
| `imageStylePrompt` | string | 图片提示词模板路径(相对于账号目录) | | `imageStylePrompt` | string | 图片提示词模板路径(相对于账号目录) |
| `videoStylePrompt` | string | 视频提示词模板路径(相对于账号目录) | | `videoStylePrompt` | string | 视频提示词模板路径(相对于账号目录) |
| `references` | array | 参考图列表每项含 file(本地文件名)和 urlOSS 公网地址) | | `references` | array | 默认参考图列表每项含 `file``url`),已被 `styles` 取代 |
| `capcut.effects` | string[] | CapCut 特效名称列表 | | `capcut.effects` | string[] | CapCut 特效名称列表 |
| `capcut.filter` | string | CapCut 滤镜,格式 "名称:强度" | | `capcut.filter` | string | CapCut 滤镜,格式 `"名称:强度"`(如 `"质感电影:40"` |
| `capcut.subtitleStyle` | object | 字幕样式(字号、颜色、高亮色、加粗 | | `capcut.subtitleStyle` | object | 字幕样式(font/字体、fontSize、color、highlightColor、bold、transformY、inAnimation/outAnimation |
| `capcut.keywordStyle` | object | 关键字氛围词样式textEffect 花字、fontSize、color、动画留空或删除则关闭 |
| `capcut.transitions` | object | 转场配置strategy + default + byPosition + byDirector |
| `capcut.defaultBGM` | string | 默认背景音乐 URL | | `capcut.defaultBGM` | string | 默认背景音乐 URL |
--- ---

View File

@@ -11,7 +11,7 @@
```bash ```bash
# Step 2-A 生成 imagePrompt 后,通过脚本初始化(不含 videoPrompt # Step 2-A 生成 imagePrompt 后,通过脚本初始化(不含 videoPrompt
node scripts/pipeline.js init --account 军事账号 --mode single \ node scripts/pipeline.js init --account 军事账号 --mode single \
--items '[{"shotDesc":"英文画面描述","script":"中文口播文案","duration":5,"imagePrompt":"English prompt","directorRef":"tarantino"}]' --items '[{"shotDesc":"英文画面描述","script":"中文口播文案","duration":5,"imagePrompt":"English prompt","directorRef":"tarantino","keyword":"权力"}]'
# 或从文件读取 # 或从文件读取
node scripts/pipeline.js init --account 军事账号 --mode single --items-file ./items.json node scripts/pipeline.js init --account 军事账号 --mode single --items-file ./items.json

View File

@@ -120,8 +120,8 @@ function getTransition(item, index, totalCount, transitionConfig) {
const defaultT = transitionConfig.default || { name: '闪白', duration: 150000 } const defaultT = transitionConfig.default || { name: '闪白', duration: 150000 }
const strategy = transitionConfig.strategy || 'fixed' const strategy = transitionConfig.strategy || 'fixed'
// 第一个素材不加转场 // 前两段不加转场(避免开头黑屏/闪烁)
if (index === 0) return { name: '', duration: 0 } if (index <= 1) return { name: '', duration: 0 }
switch (strategy) { switch (strategy) {
case 'director': { case 'director': {
@@ -132,13 +132,11 @@ function getTransition(item, index, totalCount, transitionConfig) {
} }
case 'rhythm': { case 'rhythm': {
// 按位置选择转场hook / body / keypoint / closing // 按位置选择转场:只在 keypoint / closing 加转场,其余不加
const rules = transitionConfig.byPosition || {} const rules = transitionConfig.byPosition || {}
if (index === 1) return rules.hook || defaultT
if (index >= totalCount - 2) return rules.closing || defaultT if (index >= totalCount - 2) return rules.closing || defaultT
// 每隔3个 shot 用一个强调转场 if (index % 4 === 0) return rules.keypoint || defaultT
if (index % 3 === 0) return rules.keypoint || defaultT return { name: '', duration: 0 }
return rules.body || defaultT
} }
case 'fixed': case 'fixed':
@@ -218,7 +216,7 @@ async function assemble(args) {
format = '9:16', format = '9:16',
apiKey = '', apiKey = '',
duration = '4', duration = '4',
animation = '渐显+放大', animation = '缩放',
} = args } = args
if (!input) throw new Error('缺少 --input 参数') if (!input) throw new Error('缺少 --input 参数')
@@ -533,12 +531,16 @@ async function addImages(draftUrl, items, imgUrls, timeline, width, height, anim
transition_duration: t.duration, transition_duration: t.duration,
} }
// animation 解析:循环动画 → loop_animation其余 → in_animation
// GroupAnimationType循环缩放, 缩放 II, 回弹伸缩, 旋转伸缩, ...
// IntroType入场渐显, 放大, 缩小, 向右滑动, 轻微放大, ...
// OutroType出场渐隐, 缩小, 放大, 向左滑动, ...
if (animation) { if (animation) {
const parts = animation.split('+').map(p => p.trim()).filter(Boolean) const parts = animation.split('+').map(p => p.trim()).filter(Boolean)
const groupNames = ['缩放', '缩放 II'] const loopNames = ['缩放', '缩放 II', '回弹伸缩', '旋转伸缩']
const groupAnims = parts.filter(p => groupNames.includes(p)) const loopAnims = parts.filter(p => loopNames.includes(p))
const inAnims = parts.filter(p => !groupNames.includes(p)) const inAnims = parts.filter(p => !loopNames.includes(p))
if (groupAnims.length > 0) info.loop_animation = groupAnims.join('|') if (loopAnims.length > 0) info.loop_animation = loopAnims.join('|')
if (inAnims.length > 0) info.in_animation = inAnims.join('|') if (inAnims.length > 0) info.in_animation = inAnims.join('|')
} }