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 按模板要求输出分镜表 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 已填)
- 展示给用户确认,确认后进入 Step 2-A
@@ -136,7 +136,7 @@ Step 2-B: 生成静态分镜图 + Manifest 初始化
- 组装 items 并初始化 manifest**不含 videoPrompt**
```bash
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
- 所有 item.confirmed = false
@@ -183,7 +183,7 @@ node scripts/pipeline.js validate-account --account <id>
# 初始化 manifestStep 2-B 使用AI 只提供创意内容,不含 videoPrompt
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适合大量数据
node scripts/pipeline.js init --account <id> --mode single --items-file ./items.json
@@ -354,8 +354,6 @@ output/{name}_{YYYYMMDD}_{NNN}/
---
---
## 质量卡点Agent 可执行)
每个阶段完成后 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)
- `references/account-system.md` — 账号系统说明

View File

@@ -37,25 +37,62 @@ accounts/ # 项目根目录下
"id": "tech-talk",
"name": "科技解说",
"description": "科技类短视频账号,深色背景,赛博朋克风格",
"pipeline": "image-video",
"defaultFormat": "9:16",
"imageModel": "gemini",
"videoModel": "kling",
"videoModel": "veo3-fast",
"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",
"imageStylePrompt": "prompts/图片提示词.md",
"videoStylePrompt": "prompts/视频提示词.md",
"references": [
{ "file": "ref_001.png", "url": "https://oss.../ref_001.png" }
],
"references": [],
"capcut": {
"effects": ["录制边框 III"],
"filter": "电影感:40",
"filter": "质感暗调:30",
"subtitleStyle": {
"fontSize": 36,
"fontSize": 24,
"color": "#FFFFFF",
"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 | 账号唯一标识(与目录名一致) |
| `name` | string | 账号显示名 |
| `description` | string | 一句话描述 |
| `defaultFormat` | string | 默认画幅9:16 / 16:9 / 1:1 / 4:3 |
| `imageModel` | string | 默认图片模型 |
| `videoModel` | string | 默认视频模型 |
| `pipeline` | string | 流水线类型:`image-video`(默认 |
| `defaultFormat` | string | 默认画幅(`9:16` / `16:9` / `1:1` / `4:3` |
| `imageModel` | string | 默认图片模型(`gemini` / `mj` |
| `videoModel` | string | 默认视频模型(`veo3-fast` / `grok-video-3` / `kling` |
| `batchSize` | number | 默认批量生成数量 |
| `styles` | object | 命名风格预设,每项含 `references` 数组 |
| `ttsVoice` | string | TTS 音色 ID留空用 config.json 全局默认 |
| `ttsInstruction` | string | TTS 语气指令(描述期望的语气、语速、情感) |
| `storyboardPrompt` | string | 分镜提示词模板路径(相对于账号目录) |
| `imageStylePrompt` | string | 图片提示词模板路径(相对于账号目录) |
| `videoStylePrompt` | string | 视频提示词模板路径(相对于账号目录) |
| `references` | array | 参考图列表每项含 file(本地文件名)和 urlOSS 公网地址) |
| `references` | array | 默认参考图列表每项含 `file``url`),已被 `styles` 取代 |
| `capcut.effects` | string[] | CapCut 特效名称列表 |
| `capcut.filter` | string | CapCut 滤镜,格式 "名称:强度" |
| `capcut.subtitleStyle` | object | 字幕样式(字号、颜色、高亮色、加粗 |
| `capcut.filter` | string | CapCut 滤镜,格式 `"名称:强度"`(如 `"质感电影:40"` |
| `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 |
---

View File

@@ -11,7 +11,7 @@
```bash
# Step 2-A 生成 imagePrompt 后,通过脚本初始化(不含 videoPrompt
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

View File

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