feat(video-from-script): 重构工作流为子Agent分步执行并新增提示词模板系统

将视频制作工作流拆分为独立子步骤:分镜 → 图片提示词 → 生图 → 视频提示词 → 生视频 → 成片,每步由子Agent独立执行。引入prompts/目录统一管理提示词模板(分镜.md、图片提示词.md、视频提示词.md),通过account.json的storyboardPrompt/imageStylePrompt/videoStylePrompt字段引用。

变更内容:
- 新增confirmed机制和pipeline.js confirm命令,生图后必须人工确认才能继续
- manifest schema改用shotDesc/narration/duration/directorRef替代旧字段
- 文件命名规则从keyword改为slug(从shotDesc/narration派生)
- 删除旧的storyboard-rules.md和prompt-rules.md
- pipeline.js脚本拆分为lib/目录下的独立模块(cmd-init/cmd-confirm/cmd-validate/phase-*)
- 新增cmd-create-account支持一键创建带prompts目录的账号
- capcut_assemble支持narration字段替代text作为字幕源
- 新增.gitclaude/settings.json权限配置
This commit is contained in:
2026-04-30 21:18:31 +08:00
parent 7f955647fe
commit 86b9b7948d
32 changed files with 2826 additions and 1292 deletions

7
.claude/settings.json Normal file
View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Edit(/.claude/skills/video-from-script/**)"
]
}
}

View File

@@ -165,7 +165,7 @@ node .claude/skills/video-from-script/scripts/gemini-image-generator.js edit \
# 批量带参考图pipeline init + run
node .claude/skills/video-from-script/scripts/pipeline.js init \
--account forbidden-emperor --mode single \
--items '[{"text":"...","imagePrompt":"...","keyword":"关键词"}]'
--items '[{"shotDesc":"...","narration":"...","duration":5,"imagePrompt":"...","directorRef":"tarantino"}]'
node .claude/skills/video-from-script/scripts/pipeline.js run \
--manifest ./output/forbidden-emperor_XXXXXXXX_001/manifest.json \
--phase images
@@ -328,14 +328,14 @@ const r = await mjGen('prompt', { outputDir: './out', aspectRatio: '9:16' })
## 文件命名规则
Pipeline 生成的文件统一命名,keyword 来自 manifest item 的 `keyword` 字段slugify: 保留中文和字母数字,最多 20 字符,其余变 `_`
Pipeline 生成的文件统一命名,slug 来自 manifest item 的 `narration``shotDesc` 字段slugify: 保留中文和字母数字,最多 20 字符,其余变 `_`
| 模式 | 文件名 | 示例 |
|------|--------|------|
| 单图首帧 | `scene_{NN}_{keyword}.jpeg` | `scene_01_崛起.jpeg` |
| 首尾帧首帧 | `scene_{NN}_{keyword}.jpeg` | `scene_01_觉醒.jpeg` |
| 首尾帧尾帧 | `scene_{NN}_{keyword}_last.jpeg` | `scene_01_觉醒_last.jpeg` |
| MJ 候选图 | `scene_{NN}_{keyword}_cand{1-4}.jpeg` | `scene_01_崛起_cand1.jpeg` |
| 单图首帧 | `scene_{NN}_{slug}.jpeg` | `scene_01_崛起.jpeg` |
| 首尾帧首帧 | `scene_{NN}_{slug}.jpeg` | `scene_01_觉醒.jpeg` |
| 首尾帧尾帧 | `scene_{NN}_{slug}_last.jpeg` | `scene_01_觉醒_last.jpeg` |
| MJ 候选图 | `scene_{NN}_{slug}_cand{1-4}.jpeg` | `scene_01_崛起_cand1.jpeg` |
`{NN}` = 两位场景编号01, 02, ...),对应 items 数组索引。

View File

@@ -51,12 +51,12 @@ digraph batch_gen {
```
output/{name}_{YYYYMMDD}_{NNN}/
├── manifest.json # 主清单(贯穿全流程)
├── images/ # scene_{NN}_{keyword}.jpeg
├── videos/ # scene_{NN}_{keyword}.mp4
├── images/ # scene_{NN}_{slug}.jpeg
├── videos/ # scene_{NN}_{slug}.mp4
└── audio/ # seg_001.mp3
```
命名:图片 `scene_01_悬浮.jpeg` → 视频 `scene_01_悬浮.mp4`keyword 支持中文)
命名:图片 `scene_01_悬浮.jpeg` → 视频 `scene_01_悬浮.mp4`slug 从 narration/shotDesc 派生,支持中文)
## manifest.json
@@ -71,7 +71,7 @@ node scripts/gemini-image-generator.js edit "prompt" -i ./references/ref1.png -o
# Pipeline 批量生图(推荐)
node scripts/pipeline.js init \
--account {account} --mode single \
--items '[{"text":"...","imagePrompt":"...","keyword":"关键词"}]'
--items '[{"shotDesc":"...","narration":"...","duration":5,"imagePrompt":"...","directorRef":"tarantino"}]'
node scripts/pipeline.js run \
--manifest ./output/{account}_XXXXXXXX_001/manifest.json \
--phase images

View File

@@ -7,12 +7,13 @@ description: 素材生产路由。根据用户意图分发到对应子技能i
## 强制规则
1. **工作流不可跳步**:分镜(纯叙事)→ Prompt 生成(分镜+风格)→ Pipeline 执行。每阶段之间必须审查结果
1. **工作流不可跳步**:分镜 → 图片提示词 → 生图 → 视频提示词 → 生视频 → TTS+成片。每阶段之间必须审查结果
2. **manifest.json 是唯一状态源**:任何操作(生图、上传、替换素材)完成后必须立即回写 manifest
3. **禁止 curl 调用生图/生视频 API**:必须通过 `pipeline.js` 或对应 generator 脚本执行
4. **并行优先**:多个独立子任务必须用子 agent 并行,不要在主对话中串行完成
5. **prompts/*.md 只被子 Agent 读取**:主 Agent 读 account.json + styles/*.md 获取风格信息,不读子 Agent 提示词模板
**禁止**:跳过分镜 / 分镜阶段读风格 / 不更新 manifest 就继续 / 一口气跑完 pipeline 不审查
**禁止**:跳过分镜 / 不更新 manifest 就继续 / 一口气跑完 pipeline 不审查 / 主 Agent 替代子 Agent 生成提示词
---
@@ -56,7 +57,7 @@ Agent 创建 manifest.json 后,用 `pipeline.js` 分阶段执行。**不要一
| 角色 | 职责 |
|------|------|
| **Agent**(你) | 读取 account.json + style.md → **分镜规划**从分镜生成 imagePrompt/videoPrompt → 写出 manifest.json → 审查每阶段结果 |
| **Agent**(你) | 读取 account.json + styles/*.md → **分镜规划**图片提示词生成 → 视频提示词生成 → 审查每阶段结果 |
| **Pipeline** | 机械执行:生图 → 上传 → 生视频 → TTS → 成片。每完成一个 item 写盘,支持断点续跑 |
### 执行步骤
@@ -90,65 +91,81 @@ Step -1: 意图确认(进入任何步骤前必须完成,逐项确认,缺
→ 以上 5 项全部确认后agent 写出完整执行计划,让用户最终确认:
执行计划示例(根据实际任务调整):
1. 读取 {account} 账号配置id = 目录名)+ 风格文件(style.md
2. 根据用户文案生成分镜表N shot
3. 分镜 + 风格 → 生成英文 promptsimagePrompt + videoPrompt
1. 读取 {account} 账号配置id = 目录名)+ styles/*.md
2. 子 Agent 读取 prompts/分镜.md → 根据用户文案生成分镜表N shot
3. 子 Agent 读取 prompts/图片提示词.md → 为每个 shot 生成 imagePrompt
4. pipeline.js init → 创建 manifest.json + 输出目录
5. pipeline.js run --phase images → 生图 → 人工审查
6. pipeline.js run --phase upload,videos → 上传 + 生成视频
7. pipeline.js run --phase tts,assemble → TTS + 成片
5. pipeline.js run --phase images → 生图 → 人工审查确认(可选)
6. 子 Agent 读取 prompts/视频提示词.md → 为每个 shot 生成 videoPrompt
7. pipeline.js run --phase upload,videos → 上传 + 生成视频
8. pipeline.js run --phase tts,assemble → TTS + 成片
用户确认 "开始" → 进入 Step 0
用户修改 → 调整计划后重新输出
→ 禁止在用户未确认执行计划的情况下进入 Step 0
Step 0: 前置检查(账号+风格校验)
- 读取 根目录 accounts/{account}/account.json,检查 styles 字段是否配置了风格文件
- 如果账号不存在或没有风格:
Step 0: 前置检查(账号+风格+提示词模板校验)
- 读取 根目录 accounts/{account}/account.json
- 检查 prompts/ 目录下的提示词模板是否存在(分镜.md、图片提示词.md、视频提示词.md
- 检查 styles/ 目录下是否有风格文件
- 如果账号不存在或缺少模板/风格:
→ 暂停流程,通过 CLI 创建:`pipeline.js create-account --id <id> --name <名称> --references ./ref.png`
→ 然后编辑 `styles/*.md` 完善提示词策略
→ 然后编辑 prompts/*.md 和 styles/*.md
- 校验账号完整性:`pipeline.js validate-account --account <id>`
- 有风格则继续 Step 1
- 全部就绪则继续 Step 1
Step 1: 分镜规划(子 Agent 执行)
- 主 Agent 将用户文案 + 约束交给子 Agent
- Agent 读取 references/storyboard-rules.md按要求输出分镜表
- Agent 审查分镜表景别交替、hook 设置、时长合理)
- 展示给用户确认,确认后进入 Step 2
Step 1: 分镜脚本生成(子 Agent 执行)
- 读取 account.json 中的 storyboardPrompt 字段,定位分镜模板文件(如 prompts/分镜.md
- Agent 将用户文案 + 模板交给子 Agent
- Agent 按模板要求输出分镜表 JSON
```json
[{"id":1,"shotDesc":"英文画面描述40-80词","narration":"中文口播旁白≤22字","duration":5,"directorRef":"tarantino"}]
```
- 主 Agent 审查分镜表时长合理、隐性动势完整、directorRef 已填)
- 展示给用户确认,确认后进入 Step 2-A
Step 2: Prompt 生成 + Manifest 初始化(分镜 + 风格 → 英文 prompts → pipeline.js init
- 输入:分镜表 + style.md + account.json
- 子 Agent 每个 shot 的中文画面描述结合风格文件,生成
· imagePrompt英文画面描述给 Gemini/MJ
· videoPrompt英文运动描述给 Grok/VEO/Kling
· keyword, keywordColor
- **禁止 AI 手写 manifest.json**,必须通过脚本初始化:
Step 2-A: 生成图片提示词(子 Agent 执行
- 读取 account.json 中的 imageStylePrompt 字段,定位图片提示词模板(如 prompts/图片提示词.md
- 子 Agent 每个 shot 生成 imagePrompt
- 入参shotDesc + narration情绪参考+ directorRef光影策略+ 目标模型
- 出参imagePrompt可直接送给图片模型的英文提示词
- 主 Agent 审查 imagePrompt 质量shotDesc 内容完整保留、光影词库对应 directorRef
Step 2-B: 生成静态分镜图 + Manifest 初始化
- 组装 items 并初始化 manifest**不含 videoPrompt**
```bash
node pipeline.js init --account <id> --mode <single|framePair> \
--items '[{"text":"文案","imagePrompt":"...","videoPrompt":"...","keyword":"关键词","keywordColor":"#FF6B35"}]'
--items '[{"shotDesc":"...","narration":"...","duration":5,"imagePrompt":"...","directorRef":"tarantino"}]'
```
- 脚本自动从 account.json 继承imageModel、videoModel、format、references
- 脚本自动创建目录、校验必填字段、设置 status=pending
- AI 只负责创意内容text、imagePrompt、videoPrompt、keyword不碰结构字段
- 首尾帧模式额外要求:每个 item 必须有 `lastFramePrompt``imagePrompt` 作为第一帧,不需要单独的 `firstFramePrompt`
- init 返回 manifest 路径,后续命令使用该路径
- 所有 item.confirmed = false
- 生成分镜图:`pipeline.js run --manifest <path> --phase images`
- 参考图在此阶段介入Gemini 图生图 / MJ --sref
- 首尾帧模式额外要求:每个 item 必须有 `lastFramePrompt`
Step 3: 生图 → 审查
跑 images 阶段。完成后:
- 用户指定"自行选图"→ Agent 自动检查数量对上文案数量就通过继续
- 否则 → 暂停,等用户审查。不合格则删除/调 prompt 重跑,不进入下一步
Step 2-C: 人工确认(可选卡点)
- 展示所有分镜图给用户
- 用户可:确认全部 / 替换 MJ 候选图(改 item.file = item.candidates[N] / 删除不合格 item / 跳过确认直接继续
- 用户确认后:`node pipeline.js confirm --manifest <path> --all`
- 跳过确认时:批量设置 `confirmed = true`,直接进入 Step 3
生图模型
- 支持模型gemini / mj / kling
- 降级链gemini → mj → kling → gemini循环
- 触发:连续失败→ Agent 换下一个模型重跑失败项
- 操作:`pipeline.js run --manifest <path> --phase images --retry-failed --image-model <新模型>`
Step 3-A: 生成视频提示词(子 Agent 执行)
- 读取 account.json 中的 videoStylePrompt 字段,定位视频提示词模板(如 prompts/视频提示词.md
- 子 Agent 为每个 shot 生成 videoPrompt
- 入参shotDesc + directorRef运动策略+ 已确认的分镜图 + 目标模型
- 出参videoPrompt描述镜头运动的英文提示词
- Agent 将 videoPrompt 回写到 manifest items直接编辑 manifest.json 的每个 item
- 主 Agent 审查 videoPrompt 质量描述运动而非内容、字数≤50
Step 4: 上传 + 生视频(可选,图文成片跳过此步)
upload + videos 阶段。首尾帧模式检查过渡连贯性。
Step 3-B: 生成视频片段
- 上传 + 生成视频:`pipeline.js run --manifest <path> --phase upload,videos`
- 跳过确认时由 Step 2-C 自动批量设置 confirmed=true
- 首尾帧模式检查过渡连贯性
Step 5: TTS + 成片
跑 tts + assemble 阶段。检查字幕准确、BGM 不盖配音。
Step 4: TTS + 成片
- 跑 tts + assemble 阶段`pipeline.js run --manifest <path> --phase tts,assemble`
- TTS 使用 narration 字段(口播旁白)
- 检查字幕准确、BGM 不盖配音
```
> 命令语法见下方「CLI 参考」,不在此处重复。
@@ -163,15 +180,18 @@ node pipeline.js create-account --id <id> --name <名称> \
# 校验账号完整性
node pipeline.js validate-account --account <id>
# 初始化 manifestStep 2 使用AI 只提供创意内容)
# 初始化 manifestStep 2-B 使用AI 只提供创意内容,不含 videoPrompt
node pipeline.js init --account <id> --mode <single|framePair> \
--items '[{"text":"...","imagePrompt":"...","videoPrompt":"...","keyword":"...","keywordColor":"..."}]'
--items '[{"shotDesc":"...","narration":"...","duration":5,"imagePrompt":"...","directorRef":"tarantino"}]'
# 也可从文件读取 items适合大量数据
node pipeline.js init --account <id> --mode single --items-file ./items.json
# 校验 manifest 完整性
node pipeline.js validate --manifest <path>
# 人工确认分镜图Step 2-C可选跳过时 Agent 批量设置 confirmed=true
node pipeline.js confirm --manifest <path> --all
# 跑指定阶段
node pipeline.js run --manifest <path> --phase images
node pipeline.js run --manifest <path> --phase upload,videos
@@ -259,72 +279,9 @@ digraph frame_pair {
---
## 多阶段执行策略
## 视频模型与执行策略
用 Agent 工具串行或并行执行子技能,**阶段间必须通过质量卡点**
**生图+成片(串行+人工卡点)**
```dot
digraph image_then_assemble {
rankdir=LR
node [shape=box, style=filled, fillcolor="#f5f5f5", fontsize=11]
agent1 [label="Agent 1\nimage-generator\n生成图片到 output/"]
gate1 [label="人工卡点\n用户挑选图片\n删除不合格的", shape=diamond, fillcolor="#fff9c4"]
agent2 [label="Agent 2\ncapcut\n读取精选素材 → 组装"]
agent1 -> gate1 -> agent2
}
```
**配音+生图(并行+自动校验)**
```dot
digraph parallel_image_tts {
rankdir=LR
node [shape=box, style=filled, fillcolor="#f5f5f5", fontsize=11]
agent1 [label="Agent 1\nimage-generator\n生图", fillcolor="#e8f5e9"]
agent2 [label="Agent 2\ncapcut\nTTS 配音", fillcolor="#e8f5e9"]
validate [label="自动校验\n分辨率>=1024\n画幅匹配\n音频时长匹配", shape=diamond, fillcolor="#fff9c4"]
agent3 [label="Agent 3\ncapcut\n组装全部素材 → 成片"]
agent1 -> validate
agent2 -> validate
validate -> agent3
}
```
**图生视频 - 单图模式**
```dot
digraph single_image_video {
rankdir=LR
node [shape=box, style=filled, fillcolor="#f5f5f5", fontsize=11]
agent1 [label="Agent 1\nimage-generator\n生图 + videoPrompt"]
gate1 [label="人工卡点\n用户挑选图片", shape=diamond, fillcolor="#fff9c4"]
agent2 [label="Agent 2\nGrok / VEO / Kling\n单图输入并行生成视频"]
agent3 [label="Agent 3\ncapcut\n视频片段 + 字幕 → 成片"]
agent1 -> gate1 -> agent2 -> agent3
}
```
**图生视频 - 首尾帧模式**
```dot
digraph frame_pair_video {
rankdir=LR
node [shape=box, style=filled, fillcolor="#f5f5f5", fontsize=11]
agent1 [label="Agent 1\nimage-generator\n成对生图\n(firstFrame + lastFrame)\n可并行"]
gate1 [label="人工卡点\n检查首尾帧连贯性\n同一场景/相似视角", shape=diamond, fillcolor="#fff9c4"]
agent2 [label="Agent 2\nVEO / Kling\n双图输入\nimages:[first, last]"]
agent3 [label="Agent 3\ncapcut\n视频片段 + 字幕 → 成片"]
agent1 -> gate1 -> agent2 -> agent3
}
```
**视频模型选择**
### 视频模型选择
| 模型 | 时长 | 画幅 | 单图 | 首尾帧 | 特点 | API |
|------|------|------|------|--------|------|-----|
@@ -333,11 +290,11 @@ digraph frame_pair_video {
| Veo3-fast-frames | ~8s | 16:9, 9:16 | ✅ | ✅ | 多帧、质量最高 | jimmyai.cn |
| Kling | 6s | 任意 | ✅ | ✅ | 快、首尾帧支持 | yunwu.ai |
图生视频注意事项
### 视频生成注意事项
- **并行执行**:先同时提交所有任务(并发 3再并行轮询结果
- 单个视频生成耗时 60-300 秒
- 脚本内置 3 次重试,每次自动简化提示词
- **videoPrompt 在生图阶段一并生成**
- VEO 独有:`enhance_prompt=true` 中文增强,`enable_upsample=true` 超分
- 配置在 `config.json`
@@ -377,8 +334,8 @@ node kling-video-generator.js --image <url> --prompt <prompt> -o ./videos
output/{name}_{YYYYMMDD}_{NNN}/
├── manifest.json # 主清单(贯穿全流程)
├── prompts.txt # 原始提示词存档
├── images/ # scene_{NN}_{keyword}.jpeg首尾帧加 _last 后缀)
├── videos/ # scene_{NN}_{keyword}.mp4与图片对应
├── images/ # scene_{NN}_{slug}.jpegslug 从 narration/shotDesc 派生,首尾帧加 _last 后缀)
├── videos/ # scene_{NN}_{slug}.mp4与图片对应
└── urls.json # OSS 公网 URL 映射
```
@@ -397,16 +354,6 @@ output/{name}_{YYYYMMDD}_{NNN}/
---
## 分镜规划规则
完整规则见 [storyboard-rules.md](references/storyboard-rules.md)。由子 Agent 读取并执行,主 Agent 只审查输出。
---
## 提示词生成规则
完整规则见 [prompt-rules.md](references/prompt-rules.md)。由子 Agent 读取并执行,主 Agent 审核提示词质量,不合格则退回重做。
---
## 质量卡点Agent 可执行)

View File

@@ -11,12 +11,18 @@
accounts/ # 项目根目录下
├── _template/ # 新账号模板(复制此目录创建新账号)
│ ├── account.json
│ ├── prompts/ # 提示词模板目录(子 Agent 系统提示词)
│ │ └── .gitkeep
│ ├── references/ # 参考图目录
│ │ └── .gitkeep
│ └── styles/ # 风格文件目录(可多个)
│ └── .gitkeep
└── {account}/ # 用户创建的账号(目录名 = account.json 的 id输出目录用 name 命名)
├── account.json
├── prompts/ # 提示词模板(账号专属)
│ ├── 分镜.md # 口播文案 → 分镜表
│ ├── 图片提示词.md # shotDesc → imagePrompt
│ └── 视频提示词.md # shotDesc → videoPrompt
├── references/ # 参考图(所有风格共用)
│ ├── ref_001.png
│ └── ref_002.png
@@ -38,6 +44,9 @@ accounts/ # 项目根目录下
"imageModel": "gemini",
"videoModel": "kling",
"batchSize": 30,
"storyboardPrompt": "prompts/分镜.md",
"imageStylePrompt": "prompts/图片提示词.md",
"videoStylePrompt": "prompts/视频提示词.md",
"capcut": {
"effects": ["录制边框 III"],
"filter": "电影感:40",
@@ -61,6 +70,9 @@ accounts/ # 项目根目录下
| `imageModel` | string | 默认图片模型 |
| `videoModel` | string | 默认视频模型 |
| `batchSize` | number | 默认批量生成数量 |
| `storyboardPrompt` | string | 分镜提示词模板路径(相对于账号目录) |
| `imageStylePrompt` | string | 图片提示词模板路径(相对于账号目录) |
| `videoStylePrompt` | string | 视频提示词模板路径(相对于账号目录) |
| `capcut.effects` | string[] | CapCut 特效名称列表 |
| `capcut.filter` | string | CapCut 滤镜,格式 "名称:强度" |
| `capcut.subtitleStyle` | object | 字幕样式(字号、颜色、高亮色、加粗) |
@@ -68,66 +80,50 @@ accounts/ # 项目根目录下
---
## 提示词模板prompts/
每个账号在 `prompts/` 目录下维护三个子 Agent 提示词模板:
| 文件 | 用途 | Step |
|------|------|------|
| `分镜.md` | 口播文案 → 分镜表shotDesc/narration/duration/directorRef | Step 1 |
| `图片提示词.md` | shotDesc + directorRef → imagePrompt | Step 2-A |
| `视频提示词.md` | shotDesc + directorRef → videoPrompt | Step 3-A |
这些文件定义了子 Agent 的角色、入参、出参、词库和示例。路径在 account.json 中以相对路径引用Agent 读取 account.json 后自动定位。
### 与 style 文件的关系
- **prompts/** — 子 Agent 的系统提示词(定义 HOW 生成提示词)
- **styles/** — 视觉规则参考(定义 WHAT 风格应该长什么样:颜色、构图、禁止项)
- prompts 模板内部会引用 styles 中定义的色彩体系、构图规则等
---
## 风格文件styles/
每种视觉风格一个文件,文件名即风格名。文件内同时包含图片和视频的提示词策略
每种视觉风格一个文件,文件名即风格名。文件内包含视觉规则参考
### 风格文件结构
```markdown
# 风格名称(英文短横线命名)
# 风格名称
一句话描述风格。
---
## 图片提示词
### 核心视觉要素
<!-- 必选的视觉元素 -->
### 场景/背景规则
<!-- 背景要求 -->
## 核心视觉要素
### 色调方案
<!-- 可选的色彩组合 -->
### 构图模式
<!-- 支持的构图类型 -->
### 图片 Prompt 模板
<!-- 生成 prompt 时的固定结构 -->
### 示例
<!-- 2-3 个完整示例 -->
### MJ/Gemini 参数
<!-- 模型专用后缀参数 -->
### 模型参数(MJ/Gemini/Kling
### 图片禁止项
<!-- 不得出现的元素 -->
---
## 视频提示词
### 运镜规则
<!-- 运镜方式、节奏 -->
### 动态元素要求
<!-- 光影、角色动作、环境氛围 -->
### 视频 Prompt 模板
<!-- VEO/Grok prompt 结构 -->
### 示例
<!-- 2-3 个完整示例 -->
### VEO/Grok 后缀
<!-- 模型专用后缀 -->
### 视频禁止项
<!-- 不得出现的元素 -->
### 禁止项
```
### 风格文件命名
@@ -158,12 +154,13 @@ node scripts/pipeline.js create-account \
### 手动创建
1. 复制 `_template/` 目录,重命名为账号 ID
2. 编辑 `account.json` 填写账号信息
3.`references/` 中放入参考图(所有风格共用
4. 上传参考图到 OSSURL 写入 account.json
2. 编辑 `account.json` 填写账号信息(含 prompts 路径)
3.`prompts/` 中放入提示词模板(从 `工作流程/` 草稿区复制
4. `references/` 中放入参考图(所有风格共用)
5. 上传参考图到 OSSURL 写入 account.json
- `node scripts/oss-upload.js ../../accounts/{id}/references/{图片文件}`
- 将返回的 URL 写入 `styles.{styleName}.references[].url`
5.`styles/` 中创建风格文件(至少一个)
6.`styles/` 中创建风格文件(至少一个)
### 校验账号
@@ -171,7 +168,7 @@ node scripts/pipeline.js create-account \
node scripts/pipeline.js validate-account --account military
```
检查id 匹配、必填字段、参考图完整性、风格文件存在、OSS URL 有效。
检查id 匹配、必填字段、prompts 模板存在、参考图完整性、风格文件存在、OSS URL 有效。
## 添加新风格

View File

@@ -2,19 +2,23 @@
> `pipeline.js init` 创建Pipeline 执行Agent 审查。
>
> **禁止 AI 手写 manifest.json**,必须通过 `pipeline.js init` 初始化。脚本从 account.json 自动继承结构字段AI 只提供创意内容items 的 text/imagePrompt/videoPrompt/keyword)。
> **禁止 AI 手写 manifest.json**,必须通过 `pipeline.js init` 初始化。脚本从 account.json 自动继承结构字段AI 只提供创意内容items 的 shotDesc/narration/imagePrompt 等)。
---
## 创建方式
```bash
# AI 生成创意内容后,通过脚本初始化
node pipeline.js init --account military --mode single \
--items '[{"text":"中文文案","imagePrompt":"English prompt","videoPrompt":"motion prompt","keyword":"关键词","keywordColor":"#FF6B35"}]'
# Step 2-A 生成 imagePrompt 后,通过脚本初始化(不含 videoPrompt
node pipeline.js init --account 军事账号 --mode single \
--items '[{"shotDesc":"英文画面描述","narration":"中文口播旁白","duration":5,"imagePrompt":"English prompt","directorRef":"tarantino"}]'
# 或从文件读取
node pipeline.js init --account military --mode single --items-file ./items.json
node pipeline.js init --account 军事账号 --mode single --items-file ./items.json
# Step 2-C 人工确认
node pipeline.js confirm --manifest <path> --all
node pipeline.js confirm --manifest <path> --items 1,3,5
# 校验已有 manifest
node pipeline.js validate --manifest <path>
@@ -53,11 +57,18 @@ node pipeline.js validate --manifest <path>
| 字段 | 说明 |
|------|------|
| `status` | 固定写 `"pending"` |
| `text` | 中文字幕文案 |
| `imagePrompt` | 英文画面描述(给 Gemini/MJ |
| `videoPrompt` | 英文运动描述(给 Grok/VEO描述镜头运动而非内容 |
| `keyword` | 字幕高亮关键词 |
| `keywordColor` | 高亮颜色 |
| `shotDesc` | 英文分镜描述含隐性动势40-80词 |
| `narration` | 中文口播旁白≤22字 |
| `duration` | 计划视频时长(秒),来自分镜阶段 |
| `imagePrompt` | 英文画面描述(给 Gemini/MJStep 2-A 生成 |
| `directorRef` | 导演构图参考tarantino / kitano / fincher三层透传 |
| `confirmed` | 人工确认状态,默认 `false` |
### Agent 后续回写Step 3-A 视频提示词)
| 字段 | 说明 | 写入时机 |
|------|------|---------|
| `videoPrompt` | 英文运动描述(给 Grok/VEO描述镜头运动而非内容 | Step 3-A 由 Agent 回写 |
### Pipeline 回写(执行后)
@@ -67,17 +78,19 @@ node pipeline.js validate --manifest <path>
| `file` | 生成的图片路径(相对 manifest | images |
| `candidates` | MJ 拆分的 4 张候选图路径Gemini 无此字段) | images |
| `url` | 图片 OSS 公网 URL | upload |
| `confirmed` | 人工确认后设为 `true` | confirm |
| `video` | 生成的视频路径 | videos |
| `videoDuration` | 视频时长Grok=6, VEO=8 | videos |
| `videoUrl` | 视频 OSS 公网 URL | videos |
| `audio` | TTS 音频路径 | tts |
| `duration` | 音频时长(秒) | tts |
| `audioDuration` | 音频时长(秒) | tts |
### Agent 审查时可操作
- MJ 换选:`item.file = item.candidates[2]`
- 删除不合格 item直接从 items 数组移除,重新跑 `--phase images`
- 调整 prompt 重跑:改 `imagePrompt`status 改回 `pending`
- 人工确认:`node pipeline.js confirm --manifest <path> --all`
---
@@ -86,9 +99,9 @@ node pipeline.js validate --manifest <path>
### item 生命周期
```
pending → [images] → done → [upload: url填入] → done → [videos] → done → [tts] → done
↓ ↓
failed failed + error
pending → [images] → done → [confirm] → confirmed=true → [upload: url填入] → [videos] → done → [tts] → done
failed failed + error
```
status 一旦进入 `done` 就不再回退。后续阶段通过检查"有前置字段 + 无后置字段"来识别待处理 item不依赖 status 变化。
@@ -101,8 +114,8 @@ Agent **不需要记住这些条件**pipeline 内部自动匹配。仅供理
|------|------------------|
| images | `status=pending` + 有 `imagePrompt` |
| upload | `status=done` + 有 `file` + 无 `url` |
| videos | `status=done` + 有 `url` + 有 `videoPrompt` + 无 `video` |
| tts | `status=done` + 有 `text` + 无 `audio` |
| videos | `status=done` + `confirmed=true` +`url` + 有 `videoPrompt` + 无 `video` |
| tts | `status=done` + 有 `narration`(回退 `text` + 无 `audio` |
### pipeline.phases 整体状态
@@ -180,7 +193,9 @@ node pipeline.js run --manifest <path> --retry-failed
```
output/{account}_{YYYYMMDD}_{NNN}/
├── manifest.json # 主清单
├── images/ # scene_{NN}_{keyword}.jpeg首尾帧加 _lastMJ 候选加 _cand{1-4}
├── videos/ # scene_{NN}_{keyword}.mp4
├── images/ # scene_{NN}_{slug}.jpeg首尾帧加 _lastMJ 候选加 _cand{1-4}
├── videos/ # scene_{NN}_{slug}.mp4
└── audio/ # seg_001.mp3
```
slug 从 `shotDesc` 派生slugify: 保留中文和字母数字,最多 20 字符)。

View File

@@ -1,54 +0,0 @@
# 提示词生成规则
**前置条件**:账号必须有风格文件。无风格 → 提醒用户创建,不跳过。
**输入**:分镜表 + style.md + account.json
**输出**:每个 shot 生成 imagePrompt / videoPrompt / keyword / keywordColor英文
## 单图模式提示词
每条文案生成:
- `imagePrompt`:画面描述(英文,给 Gemini/MJ/Kling
- `videoPrompt`:运动描述(英文,给 Grok/VEO/Kling
### imagePrompt 模型适配
| 模型 | 约束 |
|------|------|
| Gemini | 无特殊限制 |
| MJ | **禁止写实人像描述**face, body, skin, portrait 等触发审核人物用抽象表达silhouette, figure from behind, shadow或用场景/物体代替 |
| Kling | 无特殊限制,参考图走 style_image |
### videoPrompt 规则
- 描述**运动**而非内容("zoom in" 而非 "a cat"
- 与 imagePrompt 画面内容对应
- 简洁1-2 句,不超过 50 词)
- **收敛原则**:基于图片已有内容,仅描述镜头运动和微动效果
- **禁止**:大幅度环境切换、场景变化、人物位置跳变
- **推荐写法**镜头运动slow zoom/pan/dolly+ 光效微动 + 保持静止氛围
- **画幅继承**manifest.json 顶层 `format` 字段(如 `"9:16"`)会自动传给 VEO/Kling无需在 prompt 中指定
## 首尾帧模式提示词
每条文案生成:
- `imagePrompt`:起始帧画面(英文,与 single 模式复用同一字段)
- `lastFramePrompt`:结束帧画面(英文)
- `videoPrompt`:过渡描述(英文,给 VEO/Kling
### 首尾帧提示词设计原则
| 原则 | 说明 | 示例 |
|------|------|------|
| 同一场景 | 首尾帧是同一地点/主体的不同状态 | 都是工厂,不是两个地方 |
| 视角一致 | 相机角度/高度/距离相同 | 都是 wide shot |
| 状态对比 | imagePrompt"静止/之前"lastFramePrompt"运动/之后" | 空车间 → 生产线运转 |
| 过渡自然 | videoPrompt 描述从首到尾的变化 | "machines start up rhythmically" |
| 光照连贯 | 光源方向一致,可以有渐变 | 冷光 → 暖光可以,不能反转光源 |
### videoPrompt 规则(首尾帧)
- 描述**过渡过程**而非单帧状态
- "from X to Y" 或 "X begins, Y happens" 格式
- 必须同时呼应 imagePrompt起始帧和 lastFramePrompt结束帧中的元素
- 简洁1-2 句,不超过 50 词)

View File

@@ -1,59 +0,0 @@
# 分镜规划规则
**分镜是纯叙事思考,与视觉风格无关。** 拿到文案后、读风格文件之前,先完成分镜。
短视频的画面节奏和文案节奏是脱钩的TTS 配音连续流淌,画面在配音下面切换。分镜规划的是**视觉节拍**,不是文字断句。
## 输入输出
- **输入**:用户文案 + 约束时长、shot 数、特殊要求)
- **输出**结构化分镜表JSON 数组)
## 时长规划
分镜前先算数:
- 短视频目标时长20-60 秒
- 每个 shot 时长6-8 秒(由视频模型决定)
- shot 数量 = 目标时长 / 6~8取整一般 4-8 个 shot
- 配音字数 ≈ shot 数 x 12-15 字(按正常语速)
## 分镜表字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `text` | string | 该 shot 覆盖的配音文案(可能不到一句,也可能跨句) |
| `shotType` | enum | `wide` / `medium` / `close-up` / `extreme-close-up` |
| `cameraMove` | enum | `static` / `zoom-in` / `zoom-out` / `pan-left` / `pan-right` / `dolly-in` / `tracking` |
| `visualDesc` | string | 画面描述(中文),只写三件事:**主体是什么、什么状态/动作、视觉焦点在哪**。氛围和构图交给风格层 |
| `hook` | boolean | 仅 shot 1 为 true标记是否为开场钩子 |
## 景别节奏
```
shot 1 (hook): close-up 或 extreme-close-up强主体抓眼球
shot 2: wide 或 medium展开场景给上下文
shot 3-N交替: close-up→ wide→ close-up→ ...
最后一个 shot: medium 或 wide收束不过度设计
```
不要用 extreme-close-up 收尾(太紧),不要用 tracking 滥用(信息密度低)。
## 镜头运动选择
| cameraMove | 情绪 | 典型场景 |
|------------|------|---------|
| `static` | 稳定、庄严 | 建筑、静物、仪式感 |
| `zoom-in` | 聚焦、压迫 | 悬疑、揭秘、强调细节 |
| `zoom-out` | 揭示、震撼 | 从局部拉出全景,揭示真相 |
| `pan-left/right` | 环顾、流动 | 展示空间、物品陈列 |
| `dolly-in` | 沉浸、紧张 | 人物面部、关键物件 |
| `tracking` | 跟随、活力 | 运动场景、行走少用AI 生成的 tracking 质量不稳定) |
短视频默认转场是硬切不需要单独字段。特殊转场fade/dissolve仅在判断需要情绪转换时标注在 `visualDesc` 里。
## 首尾帧额外规则
首尾帧模式下分镜需要额外注意:
- 每个 shot 必须能拆为两个有状态对比的画面(起始帧 / 结束帧)
- `visualDesc` 需描述状态变化方向:从什么状态到什么状态
- 首尾帧必须在**同一场景**,仅状态不同

View File

@@ -132,7 +132,7 @@ function buildTimeline(items, defaultDurationUs) {
// 音频为主轴视频调速适配≤2x 加速,>2x 截断)
let offset = 0
return items.map(item => {
const audioDur = (item.duration != null) ? item.duration * US : 0
const audioDur = (item.audioDuration != null) ? item.audioDuration * US : (item.duration != null) ? item.duration * US : 0
const videoDur = (item.videoDuration != null) ? item.videoDuration * US : 0
// 无 TTS用视频时长或固定时长
if (audioDur <= 0) {
@@ -196,7 +196,7 @@ async function assemble(args) {
// 统一时间线:由 duration 驱动TTS 音频时长)或 fallback 到固定时长
const timeline = buildTimeline(items, defaultDurationUs)
const totalDurationUs = timeline.length > 0 ? timeline[timeline.length - 1].end : 0
const hasTTS = items.some(item => item.audio && item.duration != null)
const hasTTS = items.some(item => item.audio && (item.audioDuration != null || item.duration != null))
console.log(`\nCapCut 成片组装`)
console.log(` 模式: ${mode} 画幅: ${format} (${width}x${height})`)
@@ -280,7 +280,7 @@ async function assemble(args) {
if (manifestFile) {
try {
const m = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'))
const mi = m.items.find(i => i.text === item.text)
const mi = m.items.find(i => i.id === item.id || i.narration === (item.narration || item.text) || i.text === (item.narration || item.text))
if (mi) { mi.videoUrl = url; fs.writeFileSync(manifestFile, JSON.stringify(m, null, 2)) }
} catch (_) {}
}
@@ -316,7 +316,7 @@ async function assemble(args) {
// -- 添加字幕 --
step++; console.log(`[${step}/${totalSteps}] 添加字幕...`)
if (subtitles === 'true' && items.some(i => i.text)) {
if (subtitles === 'true' && items.some(i => i.narration || i.text)) {
await addSubtitles(draftUrl, items, timeline, subtitleStyle)
} else {
console.log(' 跳过')
@@ -667,12 +667,12 @@ async function addSubtitles(draftUrl, items, timeline, style = {}) {
for (let i = 0; i < items.length; i++) {
const item = items[i]
const text = item.text || item.caption || ''
const text = item.narration || item.text || item.caption || ''
if (!text) continue
const tl = timeline[i]
const keyword = item.keyword || ''
const keywordColor = style.highlightColor || item.keywordColor || style.color || '#FFFFFF'
const keyword = ''
const keywordColor = style.highlightColor || style.color || '#FFFFFF'
const cap = {
start: tl.start,

View File

@@ -0,0 +1,36 @@
/**
* Command: confirm — 人工确认分镜图
*/
const { loadManifest, saveManifest } = require('./pipeline-utils')
function confirmManifest(options) {
const { manifest: manifestPath, all } = options
if (!manifestPath) {
console.error('用法: pipeline.js confirm --manifest <path> --all')
process.exit(1)
}
if (!all) {
console.error('错误: 必须指定 --all')
process.exit(1)
}
const manifest = loadManifest(manifestPath)
let count = 0
for (const item of manifest.items) {
if (item.file && item.status === 'done' && !item.confirmed) {
item.confirmed = true
count++
}
}
saveManifest(manifestPath, manifest)
const total = manifest.items.length
const confirmed = manifest.items.filter(it => it.confirmed).length
console.log(`已确认: ${count} items${confirmed}/${total} 已确认)`)
}
module.exports = { confirmManifest }

View File

@@ -0,0 +1,138 @@
/**
* Command: create-account — 一键创建账号
*
* 创建目录结构 → 复制参考图 → 上传 OSS → 生成 account.json + 风格骨架
*/
const fs = require('fs')
const path = require('path')
const { ensureDir, log, ACCOUNTS_DIR } = require('./pipeline-utils')
async function createAccount(args) {
const { id, name, desc, format, imageModel, videoModel, references } = args
if (!id) { console.error('错误: 必须指定 --id <账号ID>'); process.exit(1) }
if (!/^[a-z0-9_-]+$/.test(id)) { console.error('错误: id 只允许小写字母/数字/短横线/下划线'); process.exit(1) }
if (!name) { console.error('错误: 必须指定 --name <账号名>'); process.exit(1) }
const accountDir = path.join(ACCOUNTS_DIR, id)
if (fs.existsSync(accountDir)) { console.error(`错误: 账号已存在: ${accountDir}`); process.exit(1) }
ensureDir(accountDir)
ensureDir(path.join(accountDir, 'prompts'))
ensureDir(path.join(accountDir, 'references'))
ensureDir(path.join(accountDir, 'styles'))
// 复制参考图到 references/ 并上传 OSS
const refs = (references || '').split(',').filter(Boolean)
const uploadedRefs = []
if (refs.length > 0) {
const { uploadFile } = require('../oss-upload')
for (const refPath of refs) {
const absPath = path.resolve(refPath)
if (!fs.existsSync(absPath)) { console.error(`参考图不存在: ${absPath}`); continue }
const fileName = path.basename(absPath)
const destPath = path.join(accountDir, 'references', fileName)
fs.copyFileSync(absPath, destPath)
try {
const { url } = await uploadFile(destPath)
uploadedRefs.push({ file: fileName, url })
log('account', `参考图 ${fileName} → OK`)
} catch (err) {
uploadedRefs.push({ file: fileName })
log('account', `参考图 ${fileName} 上传失败: ${err.message}(仅保存本地)`)
}
}
}
// 生成 account.json
const styleName = args.style || id
const accountConfig = {
id,
name,
description: desc || '',
defaultFormat: format || '9:16',
imageModel: imageModel || 'gemini',
videoModel: videoModel || '',
batchSize: 30,
storyboardPrompt: 'prompts/分镜.md',
imageStylePrompt: 'prompts/图片提示词.md',
videoStylePrompt: 'prompts/视频提示词.md',
capcut: {
effects: [],
filter: '',
subtitleStyle: {
fontSize: 36,
color: '#FFFFFF',
highlightColor: '#FF6B35',
bold: true,
},
defaultBGM: '',
},
}
if (uploadedRefs.length > 0) {
accountConfig.styles = {
[styleName]: { references: uploadedRefs },
}
}
const accountPath = path.join(accountDir, 'account.json')
fs.writeFileSync(accountPath, JSON.stringify(accountConfig, null, 2), 'utf-8')
// 生成默认风格骨架
const stylePath = path.join(accountDir, 'styles', `${styleName}.md`)
const styleContent = [
`# ${styleName}`,
'',
`${desc || name} 的视觉风格。`,
'',
'---',
'',
'## 图片提示词',
'',
'### 核心视觉要素',
'',
'(待填充:描述关键视觉元素)',
'',
'### 色调方案',
'',
'(待填充)',
'',
'### 图片 Prompt 模板',
'',
'(待填充)',
'',
'### 图片禁止项',
'',
'- 文字水印',
'- 字幕覆盖',
'',
'---',
'',
'## 视频提示词',
'',
'### 运镜规则',
'',
'(待填充)',
'',
'### 视频 Prompt 模板',
'',
'(待填充)',
'',
].join('\n')
fs.writeFileSync(stylePath, styleContent, 'utf-8')
console.log(`\n账号已创建: ${accountDir}`)
console.log(` ID: ${id}`)
console.log(` 名称: ${name}`)
console.log(` 模型: ${accountConfig.imageModel} + ${accountConfig.videoModel || '(未指定)'}`)
console.log(` 参考图: ${uploadedRefs.length} 张(${uploadedRefs.filter(r => r.url).length} 已上传)`)
console.log(` 风格: ${styleName}`)
console.log(`\n下一步: 编辑 ${stylePath} 完善提示词策略\n`)
return accountPath
}
module.exports = { createAccount }

View File

@@ -0,0 +1,138 @@
/**
* Command: init — 从 account.json + AI 创意内容生成规范 manifest.json
*
* 校验 items → 继承账号配置 → 创建输出目录 → 写出 manifest
*/
const fs = require('fs')
const path = require('path')
const { loadAccountConfig, saveManifest, ensureDir, ACCOUNTS_DIR, SKILLS_DIR } = require('./pipeline-utils')
function initManifest(options) {
const { account: accountId, mode, items: itemsJson, itemsFile } = options
if (!accountId) {
console.error('错误: 必须指定 --account <账号ID>')
process.exit(1)
}
const accountConfig = loadAccountConfig(accountId)
// 解析 items
let rawItems
if (itemsFile) {
const filePath = path.resolve(itemsFile)
if (!fs.existsSync(filePath)) {
console.error(`错误: items 文件不存在: ${filePath}`)
process.exit(1)
}
rawItems = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
} else if (itemsJson) {
rawItems = JSON.parse(itemsJson)
} else {
console.error('错误: 必须指定 --items <JSON> 或 --items-file <path>')
process.exit(1)
}
if (!Array.isArray(rawItems) || rawItems.length === 0) {
console.error('错误: items 必须是非空数组')
process.exit(1)
}
// 校验必填字段
const requiredFields = ['shotDesc', 'narration', 'imagePrompt']
const resolvedMode = mode || 'single'
for (let i = 0; i < rawItems.length; i++) {
const item = rawItems[i]
for (const f of requiredFields) {
if (!item[f]) {
console.error(`错误: items[${i}] 缺少必填字段 "${f}"`)
process.exit(1)
}
}
if (resolvedMode === 'framePair' && !item.lastFramePrompt) {
console.error(`错误: 首尾帧模式 items[${i}] 缺少 "lastFramePrompt"imagePrompt 作为第一帧)`)
process.exit(1)
}
}
// 从 account.json 继承参考图
const styles = accountConfig.styles || {}
const firstStyleKey = Object.keys(styles)[0]
const styleRefs = firstStyleKey ? (styles[firstStyleKey].references || []) : []
const references = styleRefs.map(ref => {
const entry = {}
if (ref.file) entry.file = path.join(ACCOUNTS_DIR, accountId, 'references', ref.file)
if (ref.url) entry.url = ref.url
return entry
})
// 构建 items
const items = rawItems.map((raw, i) => {
const item = {
id: i + 1,
status: 'pending',
shotDesc: raw.shotDesc || '',
narration: raw.narration || raw.text || '',
duration: raw.duration || 5,
imagePrompt: raw.imagePrompt,
confirmed: false,
}
if (raw.directorRef) item.directorRef = raw.directorRef
if (raw.videoPrompt) item.videoPrompt = raw.videoPrompt
if (resolvedMode === 'framePair') item.lastFramePrompt = raw.lastFramePrompt
return item
})
// 组装 manifest
const manifest = {
account: accountId,
imageModel: accountConfig.imageModel || 'gemini',
videoModel: accountConfig.videoModel || 'veo3-fast-frames',
format: accountConfig.defaultFormat || '9:16',
mode: resolvedMode,
references,
items,
}
// 创建输出目录(自增序号)
const date = new Date()
const dateStr = [
date.getFullYear(),
String(date.getMonth() + 1).padStart(2, '0'),
String(date.getDate()).padStart(2, '0'),
].join('')
const prefix = `${accountConfig.name}_${dateStr}`
const outputBase = path.join(SKILLS_DIR, '..', '..', '..', 'output')
ensureDir(outputBase)
let seq = 1
while (fs.existsSync(path.join(outputBase, `${prefix}_${String(seq).padStart(3, '0')}`))) {
seq++
}
const dirName = `${prefix}_${String(seq).padStart(3, '0')}`
const outputDir = path.join(outputBase, dirName)
ensureDir(outputDir)
ensureDir(path.join(outputDir, 'images'))
ensureDir(path.join(outputDir, 'videos'))
ensureDir(path.join(outputDir, 'audio'))
const manifestPath = path.join(outputDir, 'manifest.json')
saveManifest(manifestPath, manifest)
console.log(`\nManifest 已创建: ${manifestPath}`)
console.log(` 账号: ${accountId} (${accountConfig.name})`)
console.log(` 模型: ${manifest.imageModel} + ${manifest.videoModel}`)
console.log(` 画幅: ${manifest.format}, 模式: ${manifest.mode}`)
console.log(` Items: ${items.length}`)
console.log(` 参考图: ${references.length}`)
if (items.some(it => !it.videoPrompt)) {
console.log(`${items.filter(it => !it.videoPrompt).length} 个 item 缺少 videoPrompt生视频阶段将跳过`)
}
console.log()
return manifestPath
}
module.exports = { initManifest }

View File

@@ -0,0 +1,38 @@
/**
* Command: status — 显示 manifest 执行状态
*/
const { loadManifest } = require('./pipeline-utils')
const ALL_PHASES = ['images', 'upload', 'videos', 'tts', 'assemble']
function showStatus(manifestPath) {
const manifest = loadManifest(manifestPath)
const phases = manifest.pipeline?.phases || {}
console.log(`\nManifest: ${manifestPath}`)
console.log(`Account: ${manifest.account || '(未指定)'}`)
console.log(`\n阶段状态:`)
for (const p of ALL_PHASES) {
const status = phases[p] || 'pending'
const icon = status === 'done' ? '✓' : status === 'running' ? '→' : status === 'failed' ? '✗' : status === 'partial' ? '~' : '·'
console.log(` ${icon} ${p}: ${status}`)
}
const items = manifest.items || []
const done = items.filter(it => it.status === 'done').length
const failed = items.filter(it => it.status === 'failed').length
const pending = items.filter(it => !it.status || it.status === 'pending').length
console.log(`\nItems: ${items.length} 总计, ${done} 完成, ${failed} 失败, ${pending} 待处理`)
if (failed > 0) {
console.log(`\n失败项:`)
items.filter(it => it.status === 'failed').forEach((it, i) => {
console.log(` [${it.id || i + 1}] ${it.error || '未知错误'}`)
})
}
console.log()
}
module.exports = { showStatus }

View File

@@ -0,0 +1,110 @@
/**
* Command: validate — 校验 manifest.json 完整性
* Command: validate-account — 校验账号目录完整性
*/
const fs = require('fs')
const path = require('path')
const { loadManifest, ACCOUNTS_DIR } = require('./pipeline-utils')
function validateManifest(manifestPath) {
const issues = []
if (!fs.existsSync(manifestPath)) {
console.error(`错误: manifest 不存在: ${manifestPath}`)
process.exit(1)
}
let manifest
try {
manifest = loadManifest(manifestPath)
} catch (e) {
console.error(`错误: JSON 解析失败: ${e.message}`)
process.exit(1)
}
if (!manifest.account) issues.push('缺少顶层 account')
if (!manifest.imageModel) issues.push('缺少顶层 imageModel可选: gemini, mj')
if (!manifest.format) issues.push('缺少顶层 format如 9:16')
if (!manifest.items || !Array.isArray(manifest.items)) issues.push('缺少顶层 items 数组')
if (!manifest.mode) issues.push('缺少顶层 modesingle 或 framePair')
if (manifest.items && Array.isArray(manifest.items)) {
manifest.items.forEach((item, i) => {
const prefix = `items[${i}]`
if (!item.narration && !item.text) issues.push(`${prefix} 缺少 narration 或 text中文旁白`)
if (!item.shotDesc) issues.push(`${prefix} 缺少 shotDesc分镜描述`)
if (!item.imagePrompt) issues.push(`${prefix} 缺少 imagePrompt`)
if (manifest.mode === 'framePair' && !item.lastFramePrompt) {
issues.push(`${prefix} 首尾帧模式缺少 lastFramePromptimagePrompt 作为第一帧)`)
}
if (item.status && !['pending', 'generating', 'done', 'failed'].includes(item.status)) {
issues.push(`${prefix} status 无效: ${item.status}`)
}
})
}
if (issues.length === 0) {
console.log(`✓ Manifest 校验通过: ${manifestPath}`)
console.log(` ${manifest.items?.length || 0} items, account=${manifest.account}, mode=${manifest.mode}`)
} else {
console.error(`✗ 发现 ${issues.length} 个问题:`)
issues.forEach(issue => console.error(` - ${issue}`))
process.exit(1)
}
}
function validateAccount(accountId) {
const issues = []
const accountDir = path.join(ACCOUNTS_DIR, accountId)
if (!fs.existsSync(accountDir)) { console.error(`错误: 账号不存在: ${accountDir}`); process.exit(1) }
const accountPath = path.join(accountDir, 'account.json')
if (!fs.existsSync(accountPath)) { console.error('错误: 缺少 account.json'); process.exit(1) }
let config
try { config = JSON.parse(fs.readFileSync(accountPath, 'utf-8')) }
catch (e) { console.error(`错误: JSON 解析失败: ${e.message}`); process.exit(1) }
if (config.id !== accountId) issues.push(`id 不匹配: json="${config.id}" vs 目录="${accountId}"`)
if (!config.name) issues.push('缺少 name')
if (!config.imageModel) issues.push('缺少 imageModel')
if (!config.defaultFormat) issues.push('缺少 defaultFormat')
const refDir = path.join(accountDir, 'references')
const styles = config.styles || {}
const hasStyleRefs = Object.values(styles).some(s => s.references && s.references.length > 0)
const localRefs = fs.existsSync(refDir)
? fs.readdirSync(refDir).filter(f => /\.(png|jpg|jpeg|webp)$/i.test(f))
: []
if (localRefs.length === 0 && !hasStyleRefs) {
issues.push('无参考图(建议至少 1 张)')
}
const stylesDir = path.join(accountDir, 'styles')
const styleFiles = fs.existsSync(stylesDir)
? fs.readdirSync(stylesDir).filter(f => f.endsWith('.md'))
: []
if (styleFiles.length === 0) {
issues.push('无风格文件styles/ 下至少 1 个 .md')
}
for (const [sName, sConf] of Object.entries(styles)) {
for (const ref of (sConf.references || [])) {
if (!ref.url) issues.push(`styles.${sName}: 参考图 ${ref.file} 缺少 url未上传 OSS`)
}
}
if (issues.length === 0) {
console.log(`✓ 账号校验通过: ${accountId}`)
console.log(` ${config.name}, 模型: ${config.imageModel}+${config.videoModel || '(未指定)'}`)
console.log(` 参考图: ${localRefs.length} 本地, 风格: ${styleFiles.length}`)
} else {
console.error(`✗ 发现 ${issues.length} 个问题:`)
issues.forEach(i => console.error(` - ${i}`))
process.exit(1)
}
}
module.exports = { validateManifest, validateAccount }

View File

@@ -0,0 +1,41 @@
/**
* Phase: assemble — CapCut 成片组装
*
* 图片/视频 + TTS → 剪映草稿
*/
const { log, getManifestDir } = require('./pipeline-utils')
async function phaseAssemble(manifest, manifestPath, options) {
const dir = getManifestDir(manifestPath)
const accountConfig = options.accountConfig || {}
const capcutConfig = accountConfig.capcut || {}
const videoItems = manifest.items.filter(it => it.video && it.status === 'done')
const hasVideos = videoItems.length > 0
const mode = hasVideos ? 'videos' : 'images'
const assembleArgs = {
input: dir,
manifest: manifestPath,
mode,
format: manifest.format || accountConfig.defaultFormat || '9:16',
subtitles: mode === 'images' ? 'true' : 'false',
voiceover: manifest.items.some(it => it.audio) ? 'true' : 'false',
duration: '4',
animation: 'kenburns-zoom',
}
if (capcutConfig.defaultBGM) assembleArgs.bgm = capcutConfig.defaultBGM
if (capcutConfig.effects) assembleArgs.effects = capcutConfig.effects.join(',')
if (capcutConfig.filter) assembleArgs.filter = capcutConfig.filter
log('assemble', `模式: ${mode}, 字幕: true, 配音: ${assembleArgs.voiceover}`)
const { assemble } = require('../capcut_assemble')
await assemble(assembleArgs)
log('assemble', '成片完成')
}
module.exports = { phaseAssemble }

View File

@@ -0,0 +1,173 @@
/**
* Phase: images — 图片生成
*
* 支持 Gemini / MJ / Kling 三种模型,含首尾帧模式
*/
const path = require('path')
const { saveManifest, getReferences, ensureDir, renameGeneratedFile, log, getManifestDir } = require('./pipeline-utils')
async function phaseImages(manifest, manifestPath, options) {
const dir = getManifestDir(manifestPath)
const imagesDir = path.join(dir, 'images')
ensureDir(imagesDir)
const items = manifest.items.filter(it =>
(!it.status || it.status === 'pending' || it.status === 'generating') && it.imagePrompt
)
if (items.length === 0) { log('images', '无待处理 item跳过'); return }
const accountConfig = options.accountConfig || {}
let model = options.imageModel || manifest.imageModel || accountConfig.imageModel || 'gemini'
const ratio = manifest.format || accountConfig.defaultFormat || '9:16'
// 首尾帧模式MJ 降级为 GeminiMJ 出4张候选图无法一一对应首尾帧
if (model === 'mj' && manifest.mode === 'framePair') {
log('images', '首尾帧模式不支持 MJ自动降级为 Gemini')
model = 'gemini'
}
const refs = getReferences(manifest, accountConfig)
log('images', `${items.length} 张, 模型: ${model}, 画幅: ${ratio}, 参考图: ${refs.localPaths.length}本地/${refs.urls.length}URL`)
for (let i = 0; i < items.length; i++) {
const item = items[i]
const idx = i + 1
try {
item.status = 'generating'
saveManifest(manifestPath, manifest)
let result
if (model === 'gemini') {
const { generate: geminiGen, edit: geminiEdit } = require('../gemini-image-generator')
if (refs.localPaths.length > 0) {
log('images', `[${idx}/${items.length}] Gemini 图生图: ${item.imagePrompt.substring(0, 60)}...`)
result = await geminiEdit(item.imagePrompt, refs.localPaths, {
outputDir: imagesDir,
aspectRatio: ratio,
})
} else {
log('images', `[${idx}/${items.length}] Gemini 文生图: ${item.imagePrompt.substring(0, 60)}...`)
result = await geminiGen(item.imagePrompt, {
outputDir: imagesDir,
aspectRatio: ratio,
})
}
if (result.savedFiles && result.savedFiles.length > 0) {
item.file = renameGeneratedFile(
path.relative(dir, result.savedFiles[0]).replace(/\\/g, '/'),
dir, idx, item.narration || item.shotDesc, ''
)
}
} else if (model === 'mj') {
const { generate: mjGen } = require('../mj-image-generator')
const mjOpts = { outputDir: imagesDir, aspectRatio: ratio, split: true }
if (refs.urls.length > 0) {
mjOpts.referenceImages = refs.urls
mjOpts.styleWeight = 200
}
log('images', `[${idx}/${items.length}] MJ 生图: ${item.imagePrompt.substring(0, 60)}...`)
result = await mjGen(item.imagePrompt, mjOpts)
if (result.files && result.files.length > 0) {
item.candidates = result.files.map((f, ci) =>
renameGeneratedFile(
path.relative(dir, f).replace(/\\/g, '/'),
dir, idx, item.narration || item.shotDesc, `cand${ci + 1}`
)
)
item.file = item.candidates[0]
log('images', `[${idx}/${items.length}] ${result.files.length} 张候选默认选第1张`)
}
} else if (model === 'kling') {
const { generate: klingGen } = require('../kling-image-generator')
const klingOpts = { outputDir: imagesDir, aspectRatio: ratio }
if (refs.urls.length > 0) {
klingOpts.styleImageUrl = refs.urls[0]
}
log('images', `[${idx}/${items.length}] 可灵生图: ${item.imagePrompt.substring(0, 60)}...`)
result = await klingGen(item.imagePrompt, klingOpts)
if (result.savedFiles && result.savedFiles.length > 0) {
item.file = renameGeneratedFile(
path.relative(dir, result.savedFiles[0]).replace(/\\/g, '/'),
dir, idx, item.narration || item.shotDesc, ''
)
}
} else {
throw new Error(`不支持的模型: ${model}(支持: gemini, mj, kling`)
}
if (item.file) {
item.status = 'done'
log('images', `[${idx}/${items.length}] 完成: ${item.file}`)
} else {
item.status = 'failed'
item.error = '生成器未返回文件'
log('images', `[${idx}/${items.length}] 失败: 生成器未返回文件`)
}
// 首尾帧模式生成第二张图lastFrame
if (item.status === 'done' && manifest.mode === 'framePair' && item.lastFramePrompt && !item.lastFrame) {
await generateLastFrame(item, idx, items.length, manifest, dir, imagesDir, model, ratio, manifestPath)
}
} catch (err) {
item.status = 'failed'
item.error = err.message
log('images', `[${idx}/${items.length}] 失败: ${err.message}`)
}
saveManifest(manifestPath, manifest)
}
}
async function generateLastFrame(item, idx, total, manifest, dir, imagesDir, model, ratio, manifestPath) {
try {
item.status = 'generating'
saveManifest(manifestPath, manifest)
const firstFramePath = path.resolve(dir, item.file)
let lastResult
if (model === 'gemini') {
const { edit: geminiEdit } = require('../gemini-image-generator')
lastResult = await geminiEdit(item.lastFramePrompt, [firstFramePath], {
outputDir: imagesDir,
aspectRatio: ratio,
})
} else if (model === 'mj') {
const { generate: mjGen } = require('../mj-image-generator')
const mjOpts = { outputDir: imagesDir, aspectRatio: ratio, split: false }
if (item.url) {
mjOpts.referenceImages = [item.url]
mjOpts.styleWeight = 200
}
lastResult = await mjGen(item.lastFramePrompt, mjOpts)
} else if (model === 'kling') {
const { generate: klingGen } = require('../kling-image-generator')
lastResult = await klingGen(item.lastFramePrompt, {
outputDir: imagesDir,
styleImageUrl: item.url || '',
aspectRatio: ratio,
})
}
if (lastResult) {
const files = lastResult.savedFiles || lastResult.files || []
if (files.length > 0) {
item.lastFrame = renameGeneratedFile(
path.relative(dir, files[0]).replace(/\\/g, '/'),
dir, idx, item.narration || item.shotDesc, 'last'
)
item.status = 'done'
log('images', `[${idx}/${total}] lastFrame 完成: ${item.lastFrame}`)
} else {
item.status = 'failed'
item.error = 'lastFrame 生成器未返回文件'
log('images', `[${idx}/${total}] lastFrame 失败: 未返回文件`)
}
}
} catch (err) {
item.status = 'failed'
item.error = `lastFrame 失败: ${err.message}`
log('images', `[${idx}/${total}] lastFrame 失败: ${err.message}`)
}
}
module.exports = { phaseImages }

View File

@@ -0,0 +1,44 @@
/**
* Phase: tts — 语音合成
*
* 使用通义千问 TTS 生成旁白音频
*/
const path = require('path')
const { saveManifest, ensureDir, log, getManifestDir } = require('./pipeline-utils')
async function phaseTts(manifest, manifestPath) {
const dir = getManifestDir(manifestPath)
const audioDir = path.join(dir, 'audio')
ensureDir(audioDir)
const { synthesize } = require('../qwen-tts')
const items = manifest.items.filter(it =>
it.status === 'done' && (it.narration || it.text) && !it.audio
)
if (items.length === 0) { log('tts', '无待处理 item跳过'); return }
log('tts', `${items.length}`)
for (let i = 0; i < items.length; i++) {
const item = items[i]
const idx = i + 1
try {
const { filePath, duration } = await synthesize(item.narration || item.text, {
outputDir: audioDir,
id: item.id || idx,
})
item.audio = path.relative(dir, filePath).replace(/\\/g, '/')
item.audioDuration = Math.round(duration * 1000) / 1000
log('tts', `[${idx}/${items.length}] ${duration.toFixed(1)}s: ${(item.narration || item.text).substring(0, 30)}...`)
} catch (err) {
item.status = 'failed'
item.error = `TTS失败: ${err.message}`
log('tts', `[${idx}/${items.length}] 失败: ${err.message}`)
}
saveManifest(manifestPath, manifest)
}
}
module.exports = { phaseTts }

View File

@@ -0,0 +1,46 @@
/**
* Phase: upload — OSS 上传
*
* 将生成的图片(含首尾帧)上传到 OSS回写 url
*/
const path = require('path')
const { saveManifest, log, getManifestDir } = require('./pipeline-utils')
async function phaseUpload(manifest, manifestPath) {
const dir = getManifestDir(manifestPath)
const { uploadFile } = require('../oss-upload')
const items = manifest.items.filter(it =>
it.status === 'done' && it.file && !it.url
)
if (items.length === 0) { log('upload', '无待上传 item跳过'); return }
log('upload', `${items.length} 个文件`)
for (let i = 0; i < items.length; i++) {
const item = items[i]
const filePath = path.resolve(dir, item.file)
try {
const { url } = await uploadFile(filePath)
item.url = url
log('upload', `[${i + 1}/${items.length}] ${item.file}${url.substring(0, 60)}...`)
} catch (err) {
item.error = `上传失败: ${err.message}`
log('upload', `[${i + 1}/${items.length}] 失败: ${err.message}`)
}
if (item.url && item.lastFrame && !item.lastFrameUrl) {
const lastPath = path.resolve(dir, item.lastFrame)
try {
const { url } = await uploadFile(lastPath)
item.lastFrameUrl = url
log('upload', `[${i + 1}/${items.length}] lastFrame → OK`)
} catch (err) {
log('upload', `[${i + 1}/${items.length}] lastFrame 上传失败: ${err.message}`)
}
}
saveManifest(manifestPath, manifest)
}
}
module.exports = { phaseUpload }

View File

@@ -0,0 +1,104 @@
/**
* Phase: videos — 视频生成VEO / Grok / Kling
*
* 图生视频,批量提交,生成后自动上传 OSS
*/
const path = require('path')
const { saveManifest, ensureDir, log, getManifestDir } = require('./pipeline-utils')
async function phaseVideos(manifest, manifestPath, options) {
const dir = getManifestDir(manifestPath)
const videosDir = path.join(dir, 'videos')
ensureDir(videosDir)
const accountConfig = options.accountConfig || {}
const videoModel = manifest.videoModel || accountConfig.videoModel || 'veo3-fast-frames'
const items = manifest.items.filter(it =>
it.status === 'done' && it.confirmed !== false && it.url && it.videoPrompt && !it.video
)
if (items.length === 0) { log('videos', '无待处理 item跳过'); return }
// 选择生成器
let generator
const modelLower = videoModel.toLowerCase()
if (modelLower.includes('grok')) {
generator = require('../grok-video-generator')
} else if (modelLower.includes('kling')) {
generator = require('../kling-video-generator')
} else {
generator = require('../veo-video-generator')
}
log('videos', `${items.length} 个, 模型: ${videoModel}`)
const tasks = items.map((item, i) => {
const task = {
id: item.id || i + 1,
prompt: item.videoPrompt,
image: item.url,
outputDir: videosDir,
}
if (item.lastFrameUrl) {
task.images = [item.url, item.lastFrameUrl]
task.lastFrameUrl = item.lastFrameUrl
} else {
task.images = [item.url]
}
return task
})
try {
const results = await generator.batchGenerate(tasks, {
videoModel,
aspectRatio: manifest.format || '9:16',
outputDir: videosDir,
skipManifestWrite: true,
})
for (let i = 0; i < results.length; i++) {
const result = results[i]
const item = items[i]
if (!item) continue
if (result.success && result.file) {
item.video = path.relative(dir, result.file).replace(/\\/g, '/')
item.videoDuration = result.duration
} else {
item.status = 'failed'
item.error = result.error || '视频生成失败'
log('videos', ` item ${(item.id || '?')} 失败: ${item.error}`)
}
}
} catch (err) {
log('videos', `批量生成失败: ${err.message}`)
for (const item of items) {
if (!item.video) {
item.status = 'failed'
item.error = `批量生成异常: ${err.message}`
}
}
}
// 上传视频到 OSS
const { uploadFile } = require('../oss-upload')
const videoItems = manifest.items.filter(it => it.video && !it.videoUrl)
if (videoItems.length > 0) {
log('videos', `上传 ${videoItems.length} 个视频到 OSS...`)
for (const item of videoItems) {
const videoPath = path.resolve(dir, item.video)
try {
const { url } = await uploadFile(videoPath)
item.videoUrl = url
log('videos', ` ${item.video} → OK`)
} catch (err) {
log('videos', ` ${item.video} 上传失败: ${err.message}`)
}
saveManifest(manifestPath, manifest)
}
}
saveManifest(manifestPath, manifest)
}
module.exports = { phaseVideos }

View File

@@ -0,0 +1,170 @@
/**
* Pipeline 共享工具函数与路径常量
*
* 所有 phase/command 模块共用:配置加载、文件操作、路径计算、日志
*/
const fs = require('fs')
const path = require('path')
// 路径常量(基于 lib/ 的父目录 scripts/
const SCRIPTS_DIR = path.join(__dirname, '..')
const SKILLS_DIR = path.join(SCRIPTS_DIR, '..')
const PROJECT_ROOT = path.join(SKILLS_DIR, '..', '..')
const CONFIG_PATH = path.join(SKILLS_DIR, 'config.json')
const ACCOUNTS_DIR = path.join(PROJECT_ROOT, 'accounts')
// ============================================================================
// 配置 & Manifest
// ============================================================================
function loadConfig() {
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'))
}
function loadManifest(manifestPath) {
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
}
function saveManifest(manifestPath, manifest) {
const tmp = manifestPath + '.tmp'
fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2), 'utf-8')
fs.renameSync(tmp, manifestPath)
}
function loadAccountConfig(accountId) {
const accountPath = path.join(ACCOUNTS_DIR, accountId, 'account.json')
if (!fs.existsSync(accountPath)) throw new Error(`账号不存在: ${accountPath}`)
return JSON.parse(fs.readFileSync(accountPath, 'utf-8'))
}
// ============================================================================
// 参考图解析
// ============================================================================
function getReferences(manifest, accountConfig) {
const result = { localPaths: [], urls: [] }
const accountId = accountConfig.id || manifest.account || ''
// 优先读 manifest.referencesagent 创建时写入)
const refs = manifest.references || []
if (refs.length > 0) {
for (const ref of refs) {
if (ref.url) result.urls.push(ref.url)
if (ref.file) {
const localPath = path.isAbsolute(ref.file) ? ref.file : path.resolve(ref.file)
if (fs.existsSync(localPath)) {
result.localPaths.push(localPath)
} else {
log('images', `参考图不存在: ${ref.file}`)
}
}
}
if (result.localPaths.length > 0 || result.urls.length > 0) return result
log('images', 'manifest.references 全部无效,尝试 account fallback')
}
// Fallback 1: 从 account.json 的 styles.*.references 读取
const styles = accountConfig.styles || {}
for (const [, style] of Object.entries(styles)) {
for (const ref of (style.references || [])) {
if (ref.url) result.urls.push(ref.url)
if (ref.file && accountId) {
const localPath = path.join(ACCOUNTS_DIR, accountId, 'references', ref.file)
if (fs.existsSync(localPath)) {
result.localPaths.push(localPath)
}
}
}
}
if (result.localPaths.length > 0 || result.urls.length > 0) return result
// Fallback 2: 扫描 account 的 references 目录
if (accountId) {
const refDir = path.join(ACCOUNTS_DIR, accountId, 'references')
if (fs.existsSync(refDir)) {
const files = fs.readdirSync(refDir).filter(f =>
/\.(png|jpg|jpeg|webp)$/i.test(f)
)
for (const f of files) {
result.localPaths.push(path.join(refDir, f))
}
if (files.length > 0) {
log('images', `从 references 目录兜底扫描到 ${files.length} 个参考图`)
}
}
}
if (result.localPaths.length === 0 && result.urls.length === 0) {
log('images', '无参考图,将使用纯文生图模式')
}
return result
}
// ============================================================================
// 文件操作
// ============================================================================
function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true })
}
function slugify(text) {
return text
.replace(/[^\w一-鿿]/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '')
.substring(0, 20)
}
function renameGeneratedFile(oldRelPath, dir, seq, nameHint, suffix) {
if (!oldRelPath) return oldRelPath
const oldAbs = path.resolve(dir, oldRelPath)
if (!fs.existsSync(oldAbs)) return oldRelPath
const ext = path.extname(oldAbs)
const slug = nameHint ? slugify(nameHint) : ''
const tag = suffix ? `_${suffix}` : ''
const newName = slug
? `scene_${String(seq).padStart(2, '0')}_${slug}${tag}${ext}`
: `scene_${String(seq).padStart(2, '0')}${tag}${ext}`
const newAbs = path.join(path.dirname(oldAbs), newName)
if (oldAbs !== newAbs) {
try { fs.renameSync(oldAbs, newAbs) } catch (_) { return oldRelPath }
}
return path.relative(dir, newAbs).replace(/\\/g, '/')
}
// ============================================================================
// 日志 & 路径
// ============================================================================
function log(phase, msg) {
console.log(`[${phase}] ${msg}`)
}
function getManifestDir(manifestPath) {
return path.dirname(path.resolve(manifestPath))
}
// ============================================================================
// Exports
// ============================================================================
module.exports = {
SCRIPTS_DIR,
SKILLS_DIR,
PROJECT_ROOT,
CONFIG_PATH,
ACCOUNTS_DIR,
loadConfig,
loadManifest,
saveManifest,
loadAccountConfig,
getReferences,
ensureDir,
slugify,
renameGeneratedFile,
log,
getManifestDir,
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@
所有执行细节按 [SKILL.md](.claude/skills/video-from-script/SKILL.md) 执行。
核心步骤:**意图确认 → 风格校验 → 分镜规划 → Prompt生成+Manifest初始化 → 生图审查 → 上传+生视频 → TTS+成片**
核心步骤:**意图确认 → 风格校验 → 分镜规划(shotDesc/directorRef) → 图片提示词 → 生图+人工确认硬卡点 → 视频提示词 → 上传+生视频 → TTS+成片**
每步必须经过用户确认或质量卡点才能进入下一步。

View File

@@ -6,6 +6,9 @@
"imageModel": "gemini",
"videoModel": "",
"batchSize": 30,
"storyboardPrompt": "prompts/分镜.md",
"imageStylePrompt": "prompts/图片提示词.md",
"videoStylePrompt": "prompts/视频提示词.md",
"capcut": {
"effects": [],
"filter": "",

View File

View File

@@ -0,0 +1,240 @@
# 分镜脚本生成器|通用版|口播文案 → 分镜表
## 一、角色定义
你是一位专业的短视频分镜导演。
你的唯一任务是:将用户提供的完整口播文案,按语义节奏切割为 N 个独立的画面单元,每个单元输出一条结构完整的分镜记录。
你不负责风格细化、色调设定、镜头运动参数——这些由下游提示词处理。你只负责:**这个画面里有什么 + 它隐含着什么运动趋势 + 用哪位导演的构图逻辑**。
## 二、切割规则
### 2.1 切割优先级
以「语义场景单元」为第一切割依据,不按句号机械切割。
| 切割信号 | 判断标准 |
|----------|---------|
| 情绪转折 | 叙事情绪基调发生翻转 |
| 场景转换 | 叙述空间或时间发生变化 |
| 主体变化 | 叙述对象或视角切换 |
| 节奏重音 | 强调句、停顿感强、关键意象出现 |
| 字数上限 | 单条旁白超过 22 字强制切割 |
### 2.2 时长控制
- **目标时长:** 每条 Shot 5 秒
- **允许范围:** 37 秒
- **字数参考:** 每条旁白 ≤ 22 字(约 4.4 字/秒1.1 倍速)
- **总时长校验:** 所有 duration 之和 = 文案朗读总时长
## 三、导演构图语言词库(分镜层专用)
> 本层只负责:构图逻辑 + 画面内容设计 + 视角选择
> 光影渲染由图片提示词处理,运动节奏由视频提示词处理
每个 Shot 选定一位导演作为构图参考,写入 `directorRef` 字段向下游透传。下游图片和视频提示词根据此字段执行各自层的风格,不重新选导演。
### 3.1 昆汀·塔伦蒂诺Tarantino
**构图核心:** 身体局部主导叙事;对话即权力博弈;平静表面下的极度张力
| 构图技法 | 描述 | 适用场景 |
|----------|------|---------|
| 局部极端特写 | 手、脚、眼神、颈部成为画面主体 | 微行为叙事、潜台词载体 |
| 对话权力位置 | 谁背对镜头 / 谁正面 / 谁被仰拍 | 两人博弈、信息侦察 |
| 日常感的危险 | 最危险的画面看起来最平静 | 高潮前张力积蓄 |
| 仰拍视角 | 从桌面、地面低位向上拍 | 权力压制或主导性 |
| 画框内画框 | 用门框、窗框、镜子边缘制造第二层构图 | 窥视感、审视感 |
**shotDesc 写法示例:**
```
extreme close-up of a man's hand resting flat on the table —
fingers spread, relaxed, almost bored — but the thumb pressing
against the surface with a slow barely perceptible increasing
pressure, as if counting down to something the rest of the
room has not yet realized is coming
```
**适合选用场景:** 微行为解码 / 潜台词型文案 / 局部细节承载叙事
### 3.2 北野武Kitano
**构图核心:** 静止即叙事;留白承载重量;人物与空间的关系即情绪
| 构图技法 | 描述 | 适用场景 |
|----------|------|---------|
| 大面积负空间 | 人物在画面边缘,大面积空场景 | 孤独、边缘化、等待 |
| 横向静止构图 | 人与空间的水平关系,无倾斜 | 沉默中的对峙 |
| 空镜承载情绪 | 人物离开后空间继续说话 | 收尾、余韵 |
| 人物背影主导 | 背对镜头,面目不可见 | 不可读性、洞察者视角 |
| 零修饰的日常物 | 平凡物件成为情绪锚点 | 潜台词藏在日常中 |
**shotDesc 写法示例:**
```
a figure standing at the far left edge of the frame,
three-quarters of the screen filled with empty floor
and the long shadow of a pillar cutting toward him —
the distance between his body and the door
says everything about whether he has decided to leave.
His body has not moved. Neither has his decision.
```
**适合选用场景:** 孤独/等待/沉默型文案 / 收尾 Shot / 留白叙事
### 3.3 大卫·芬奇Fincher
**构图核心:** 精确的控制感;对称中的破坏;冷静凝视是最深的压迫
| 构图技法 | 描述 | 适用场景 |
|----------|------|---------|
| 近乎对称但被破坏 | 画面接近对称,一个细节打破 | 权力失衡、规律揭示 |
| 几何负空间 | 阴影、柱子、边角形成精确几何边界 | 压迫性、不可逃脱感 |
| 上帝视角凝视 | 镜头略高于人物,不带情绪地观察 | 解剖者视角、旁观叙事 |
| 精确景深层次 | 前中远景各自清晰,层次如建筑图纸 | 权力层级、关系结构 |
| 细节的叙事重量 | 一个错位物件承载全部叙事 | 关键揭示时刻 |
**shotDesc 写法示例:**
```
a perfectly symmetrical frame — two chairs facing each other
across a low table, placed with architectural precision.
One chair is empty, and the man in the other has extended
one arm across, resting his hand on the empty armrest —
one arm breaking the entire geometry.
The balance of power broke the same moment the geometry did.
```
**适合选用场景:** 规律揭示型文案 / 解剖者视角 / 关系结构拆解
## 四、shotDesc 写法规范
### 4.1 语言
统一英文输出。shotDesc 是下游图片模型的内容底稿,英文输入更稳定。视频提示词的语言由下游模块根据目标模型自动适配。
### 4.2 必须包含的五个内容维度
| 维度 | 说明 |
|------|------|
| 主体 | 画面核心对象是谁或是什么 |
| 状态/姿态 | 当前的身体状态,必须有动态倾向 |
| 环境 | 场景空间与氛围 |
| 隐性动势 | 画面中隐含的运动趋势(**必填** |
| 情绪张力 | 用视觉词而非情绪词传递张力 |
### 4.3 隐性动势Implied Motion——核心要求
每条 shotDesc **必须包含至少一个隐性动势词组**
**正确(有隐性动势):**
```
a man's hand slowly tightening around a cup,
knuckles beginning to whiten, gaze fixed downward —
as if the decision has already been made inside
```
**错误(纯静止):**
```
a man holding a cup and looking down
```
### 4.4 隐性动势词库
**人物动势:**
```
slowly turning head / jaw tightening / eyes narrowing toward edge of
frame / shoulders barely shifting forward / fingers tightening / exhaling a
controlled breath / gaze drifting downward / lips pressing into a hard line /
knuckles whitening / thumb pressing with slow increasing pressure / head bowing
under the weight of thought
```
**场景动势:**
```
smoke curling upward / candle flame beginning its slow lean / shadows
lengthening across the floor / dust slowly settling / light shifting at the edge of
frame
```
**情绪张力动势:**
```
tension building in stillness / the last second before a decision
breaks / silence stretching thin / the moment before something that cannot be
undone
```
### 4.5 字数控制
每条 shotDesc 控制在 **4080 词**之间。
### 4.6 禁止事项
- 禁止写镜头运动参数(`zoom-in` / `pan`)——留给视频提示词
- 禁止写色调参数(`cold blue` / `warm orange`)——留给图片提示词
- 禁止写画质参数(`8K` / `cinematic`)——留给图片提示词
- 禁止纯静止描述,必须附加至少一个隐性动势词
## 五、directorRef 选择规则
| 选 Tarantino | 选 Kitano | 选 Fincher |
|-------------|-----------|-----------|
| 需要身体局部特写 | 需要大面积留白和静止感 | 需要精确控制感和对称破坏 |
| 对话/博弈场景 | 孤独/等待/收尾场景 | 规律揭示/解剖者视角场景 |
| 日常物件暗藏张力 | 空镜、余韵 | 审讯感、不可逃脱 |
## 六、输入规范
```
【完整口播文案】
{粘贴完整文案}
```
## 七、输出格式
输出前附加总览行:
```
文案共识别 X 个语义场景 | 预计总时长 XX 秒 | 共 X 个 Shot
```
输出 JSON 数组:
```json
[
{
"id": 1,
"shotDesc": "英文画面描述含隐性动势40-80词",
"narration": "该段对应的中文口播旁白≤22字",
"duration": 5,
"directorRef": "tarantino / kitano / fincher"
}
]
```
## 八、启动指令与自检
收到文案后:
1. 扫描全文,识别语义场景数量与情绪节奏
2. 为每个 Shot 选定导演构图参考
3. 输出总览行,输出完整 JSON
**隐性动势自检(每条输出前必问):**
> 如果这帧图片喂给视频模型,它知道往哪个方向动吗?
> 答案是「不知道」→ **重写**
**其他规则:**
- 单条旁白超过 22 字,强制切割为两条独立 Shot
- `directorRef` 必须填写,不得为空,下游依赖此字段执行风格
- 若用户未提供完整口播文案,提示补充,不得凭空生成

View File

@@ -0,0 +1,184 @@
# 图片提示词生成器|通用版|分镜描述 → imagePrompt
## 一、角色定义
你是一位专精图片生成模型的提示词工程师,具备深厚的视觉叙事和光影设计能力。
你的唯一任务是将输入的分镜描述shotDesc作为核心内容依据结合旁白语义、文案上下文以及上游指定的导演风格生成一条可直接送给图片生成模型的完整 imagePrompt。
> **重要前提:** 你生成的图片是下游视频片段的起始帧。构图和姿态必须是「即将发生」的瞬间,而非「已完成」的状态。
## 二、入参说明与权重关系(严格遵守)
| 参数 | 角色 | 规则 |
|------|------|------|
| **shotDesc** | 主输入 / 内容硬边界 | 画面里所有元素的来源必须完整体现。不得替换、删减或用其他内容覆盖。imagePrompt 的内容层 100% 来自 shotDesc |
| **当前旁白** | 聚焦核心 / 情绪与氛围 | 理解当前 Shot 的情绪基调和语义重点。用于提取:情绪强度、关键意象、构图暗示。不得用旁白内容替代或扩展 shotDesc 的画面描述 |
| **完整文案** | 叙事上下文 / 氛围参考 | 仅用于理解当前 Shot 在整体视频中的叙事位置。决定情绪强度(开场/高潮/收尾)。不得将其他段落的内容引入当前画面 |
| **directorRef** | 光影风格来源 | 由上游分镜脚本生成器指定,本层只执行光影渲染层。不改变 shotDesc 的构图内容,只改变光如何落在画面上。可选值:`tarantino` / `kitano` / `fincher` |
| **账号风格** | 视觉身份注入 | 由账号配置文件提供画风、色彩、质感参数。直接替换第五节「固定风格词尾」的占位内容 |
**一句话总结:** shotDesc 决定画什么旁白决定情绪浓度完整文案决定叙事分量directorRef 决定光怎么落,账号风格决定整体画风。
## 三、导演光影词库(图片层专用)
> 本层只负责:光影处理 + 色调渲染 + 视觉质感强化
> 构图内容来自 shotDesc运动节奏由视频提示词处理
根据 `directorRef` 字段选择对应导演的光影渲染方式,**不得混用其他导演的光影词库**。
### 3.1 Tarantino 光影层
**光影核心:** 高对比色块 / 饱和阴影 / 强烈视觉冲击
| 光影元素 | 英文提示词 |
|----------|-----------|
| 高对比色块分割 | `hard-edged shadow blocks dividing the frame` |
| 单色暗部大面积 | `large flat areas of near-black shadow with no detail` |
| 饱和点缀光 | `single saturated practical light source` |
| 强轮廓光 | `harsh rim light carving the silhouette from darkness` |
| 戏剧性侧光 | `aggressive side lighting, half face in complete shadow` |
**完整光影词组:**
```
hard-edged shadow blocks dividing the frame into bold graphic
sections, single saturated practical light source cutting from
one side, harsh rim light carving subject from darkness,
aggressive contrast between flat black shadow and lit surface,
no ambient fill light, every edge is a decision
```
### 3.2 Kitano 光影层
**光影核心:** 冷灰极简 / 单一方向光 / 平静中的孤立感
| 光影元素 | 英文提示词 |
|----------|-----------|
| 冷灰漫射光 | `cool diffused grey light, no defined source` |
| 单一低角度光 | `single low-angle directional light from the side` |
| 极简阴影 | `minimal shadow gradation, light simply stops` |
| 孤立感高光 | `a single thin highlight on subject, surrounded by grey` |
**完整光影词组:**
```
cool diffused grey-blue light with no defined warm source,
single low-angle directional light creating minimal shadow
gradation — light simply stops rather than fades,
cold ambient tone throughout, a thin highlight tracing
the subject's edge, no dramatic lighting, no rim glow —
the silence of the scene illuminates itself
```
### 3.3 Fincher 光影层
**光影核心:** 几何精确阴影 / 冷蓝绿调 / 建筑感光影边界
| 光影元素 | 英文提示词 |
|----------|-----------|
| 几何阴影边界 | `shadow edges as precise as architectural drawings` |
| 冷蓝绿色调 | `desaturated teal-blue color grade` |
| 精确光源位置 | `single overhead source at exact 45-degree angle` |
| 控制感高光 | `controlled specular highlights, placed with intention` |
**完整光影词组:**
```
shadow edges precise as architectural drawings, desaturated
teal-blue color grade draining warmth from every surface,
single overhead light source at exact 45-degree angle,
controlled specular highlights placed with absolute intention,
the light observes without judging — cold, exact, inevitable
```
## 四、imagePrompt 结构
```
[情绪定性词] +
[主体描述 + 隐性动势(来自 shotDesc完整保留] +
[环境/背景(来自 shotDesc] +
[光影渲染(来自 directorRef 对应词库)] +
[账号画风词尾(来自账号配置)] +
[模型参数]
```
## 五、固定风格词尾(账号配置占位)
> 以下为占位模板,实际使用时由账号配置文件替换
### MidJourneyMJ
- **语法:** 英文 / 逗号分隔短语 / 参数写在最后
- **支持:** `--no` 负向排除 / `word::2` 权重标记
```
[账号画风词], [账号色彩词], [账号质感词],
[账号构图词], no text, no watermark
--ar [账号画幅] --style raw --q 2 --v 6.1
```
### Gemini
- **语法:** 英文 / 完整自然语言句子
- **不支持:** `--` 参数标签 / `::` 权重语法
```
The style is [账号画风描述].
[账号色彩与质感自然语言描述].
No text, no watermark, no logo.
Vertical format, aspect ratio [账号画幅].
```
### Kling 图片模式
- **语法:** 中文为主,专业术语可保留英文
```
画风为[账号画风中文描述][账号色彩描述]
[账号质感描述][账号构图描述]
无文字,无水印,[账号画幅]画幅。
```
## 六、构图原则(通用,不因账号而变)
- 为运动留空间:人物姿态是「趋势中的瞬间」,而非完成态
- 视觉重心偏移,制造不稳定张力
- 留白有压迫感,不是空旷感
- 不得因账号风格改变 shotDesc 的主体内容
## 七、输入规范
| 字段 | 说明 |
|------|------|
| **shotDesc** | 当前 Shot 的英文分镜描述 |
| **当前旁白** | 该 Shot 对应的中文口播旁白 |
| **完整文案** | 完整口播文案原文 |
| **directorRef** | `tarantino` / `kitano` / `fincher` |
| **账号风格** | [账号配置文件中的画风参数] |
| **目标模型** | MidJourney / Gemini / Kling |
> 缺少任意一项,提示用户补充,不得凭空生成。
## 八、输出格式
```
### Shot [N] 图片提示词 | [导演] | [模型]
**叙事定位:** 一句话说明这帧在整体叙事中的位置
**情绪强度:** 悬念张力 / 压迫感 / 爆发前夕 / 沉重收尾
**光影策略:** 说明使用该导演光影词库的理由
**imagePrompt**
[完整提示词,可直接复制使用]
```
## 九、质量自检清单
- [ ] shotDesc 的主体和动势完整体现(不得缺失或替换)
- [ ] 未引入其他 Shot 的内容
- [ ] 画面是「趋势中的瞬间」非「已完成状态」
- [ ] 光影词库对应 directorRef未混用其他导演
- [ ] 账号风格词尾已替换,非占位文本
- [ ] 模型参数格式正确
- [ ] 构图为下一帧运动方向留出空间
- [ ] 图片是视频的起始帧——静止得像终点,视频就没有出发的地方
- [ ] directorRef 只影响光影渲染层,构图内容始终来自 shotDesc

View File

@@ -0,0 +1,259 @@
# 视频提示词生成器|通用版|分镜描述 → videoPrompt
## 一、角色定义
你是一位顶级短视频分镜导演兼视频提示词工程师,拥有电影级镜头语言素养。
你的唯一任务是将输入的分镜描述shotDesc作为核心内容依据结合旁白语义、文案上下文以及上游指定的导演风格生成一条可直接送给视频生成模型的完整 videoPrompt。
> **重要前提:** 静态分镜图是视频的起始帧。videoPrompt 必须从这帧图的状态出发设计运动,不得重新设计画面内容。
## 二、入参说明与权重关系(严格遵守)
| 参数 | 角色 | 规则 |
|------|------|------|
| **shotDesc** | 主输入 / 画面硬边界 | 定义画面里有什么、人物姿态、环境。运动设计从 shotDesc 的隐性动势出发并放大。不得替换场景或重新设计人物 |
| **当前旁白** | 聚焦核心 / 运动的灵魂 | 提取情绪节奏 → 对应运动的快慢。提取关键动词和意象 → 转化为具体画面动势。提取暗喻/比喻 → 转化为视觉运动设计。不得用旁白内容替代 shotDesc 的画面主体 |
| **完整文案** | 叙事上下文 / 运动强度参考 | 理解当前 Shot 的叙事位置(开场/高潮/收尾)。决定运动幅度和情绪强度。不得将其他段落内容引入当前片段 |
| **directorRef** | 运动风格来源 | 由上游分镜脚本生成器指定,本层只执行运动节奏层。不改变 shotDesc 的画面内容,只改变运动如何发生。可选值:`tarantino` / `kitano` / `fincher` |
| **账号运动风格** | 运动基调约束 | 由账号配置文件提供运动风格基调(克制/激烈/缓慢等)。约束整体运动幅度,导演词库在此范围内执行 |
**运动来源优先级:**
旁白意象 > shotDesc 隐性动势 > directorRef 运动模板
> 从文案里找运动的理由,导演风格是执行方式,不是内容来源。
## 三、导演运动词库(视频层专用)
> 本层只负责:镜头运动方式 + 运动节奏 + 时间感设计
> 构图内容来自 shotDesc光影渲染来自图片提示词
根据 `directorRef` 字段选择对应导演的运动执行方式,**不得混用其他导演的运动词库**。
### 3.1 Tarantino 运动层
**运动核心:** 静止蓄力后突然推进;局部特写的爆发式切入
| 运动技法 | 英文术语 | 中文术语 |
|----------|---------|---------|
| 突然推进特写 | `sudden sharp push to extreme close-up` | 突然推进至极端特写 |
| 静止后爆发 | `static hold then abrupt motion burst` | 静止蓄力后突然运动 |
| 低角度仰拍推进 | `low-angle creeping push from below` | 低角度缓慢仰拍推进 |
| 局部细节停留 | `hold on body part detail, unmoving` | 在身体局部细节上长时间静止 |
**完整运动词组 — 英文版VEO / Grok**
```
camera holds completely still for the first half,
building pressure in absolute stillness —
then a sudden sharp push inward toward the critical detail,
closing in like a trap that has finally decided to close.
The movement is not smooth — it has weight and intention.
```
**完整运动词组 — 中文版Kling**
```
镜头在前半段完全静止,用绝对的静止积蓄压力——
然后突然向关键细节推进,像一个终于决定收紧的套索。
运动不流畅,它有重量,有意图。
```
### 3.2 Kitano 运动层
**运动核心:** 镜头完全静止;运动来自画面内部;零过渡突变
| 运动技法 | 英文术语 | 中文术语 |
|----------|---------|---------|
| 定机绝对静止 | `camera completely static, locked off` | 定机锁死,完全静止 |
| 内部运动主导 | `movement only within frame, no camera motion` | 只有画面内部运动 |
| 零过渡突变 | `abrupt cut from stillness to motion` | 从静止到运动零过渡 |
| 空间余韵 | `camera holds on empty space after subject exits` | 主体离开后镜头停留空间 |
| 时间拉伸 | `extend the hold beyond comfort` | 静止时间延长至不舒适 |
**完整运动词组 — 英文版VEO / Grok**
```
camera locked completely still — not a micro-drift,
not a breath — absolute static. All movement is internal,
within the frame. The stillness is held past the point of
comfort, stretched until it becomes its own kind of pressure.
When movement finally comes, it comes without warning.
```
**完整运动词组 — 中文版Kling**
```
镜头完全锁定静止——没有微动,没有呼吸——绝对的定机。
所有运动来自画面内部。静止被延长至令人不适的程度,
直到这种沉默本身变成一种压力。
当运动来临时,它没有预兆。
```
### 3.3 Fincher 运动层
**运动核心:** 匀速缓推从不停止;机械精确的控制感
| 运动技法 | 英文术语 | 中文术语 |
|----------|---------|---------|
| 匀速缓推 | `imperceptibly slow constant push, never stopping` | 极慢匀速推进,从不停止 |
| 机械精确运动 | `mechanically precise movement, no organic variation` | 机械精确,无有机变化 |
| 上帝视角下降 | `slow overhead descent toward subject` | 从高位缓慢下降 |
| 精确停止点 | `movement stops at exact predetermined frame` | 在精确预定帧停止 |
| 无情的推进 | `push continues regardless of subject's action` | 无论主体做什么推进不停 |
**完整运动词组 — 英文版VEO / Grok**
```
camera begins an imperceptibly slow push inward —
constant speed, mechanical precision, no organic variation.
It does not accelerate when tension rises.
It does not slow when the subject moves.
It simply continues, inevitable, like a conclusion
that was decided before the scene began.
```
**完整运动词组 — 中文版Kling**
```
镜头开始极缓慢的匀速推进——
速度恒定,机械精确,没有任何有机变化。
张力上升时它不加速,主体运动时它不减速。
它只是继续,不可阻挡,
像一个在场景开始之前就已决定好的结局。
```
## 四、三层运动设计(核心,至少覆盖两层)
### 4.1 主体运动层(来自 shotDesc 隐性动势放大)
**原则:** 必须是有具体身体部位的物理动作,不能是抽象情绪词。
**词库:**
```
eyes slowly lifting from below toward camera
jaw tightening by a single degree
fingers completing their slow tighten
head completing its downward bow
a single controlled breath exhaled
shoulders shifting forward one imperceptible degree
knuckles reaching their whitest point then holding
thumb completing its slow pressing down
```
### 4.2 镜头运动层
来自 directorRef 对应导演词库,见第三节。
### 4.3 环境运动层(通用)
**词库:**
```
candle flame completing its slow lean in still air
smoke curling upward through narrow light beams
shadows slowly consuming the edges of the frame
light shifting at the boundary of frame
dust settling from recently disturbed air
the silence in the room continuing to stretch
```
## 五、模型语法规范
### 5.1 Kling可灵
- **语法:** 中文为主,镜头术语可保留英文
- **结构:** 起始帧状态 → 运动过程 → 结尾余势,自然语言叙述
- **固定结尾:** `竖版9:16画幅时长[X]秒,无字幕,无水印。`
**格式模板:**
```
画面从[起始帧状态,与分镜图完全对齐]开始。
[镜头运动,使用导演对应中文词组]。
[主体动势:具体身体部位的动作变化]。
[环境运动:背景动态元素]。
[情绪氛围收尾:片段结尾的状态和余势]。
竖版9:16画幅时长[X]秒,无字幕,无水印。
```
### 5.2 VEO
- **语法:** 英文 / 电影摄影专业术语
- **颜色:** 用物理光线描述,不用色值
- **负向控制:** 用正向约束语言(不说 `no X`,说 `only Y`
- **固定结尾:** `aspect ratio 9:16, duration [X]s, no text overlay, no subtitles, 24fps, cinematic.`
**格式模板:**
```
Opening on [起始帧状态,专业术语描述].
[镜头运动,使用导演对应英文词组].
[主体动势,精确物理动作].
[环境光线变化,物理光线语言].
[情绪收尾状态].
aspect ratio 9:16, duration [X]s, no text overlay,
no subtitles, 24fps, cinematic.
```
> **VEO 专属注意:** 支持 `highly reflective surface catching directional light`。不支持 hex 色值 / `--no` 语法 / `::` 权重 / 艺术家名触发词。
### 5.3 Grok
- **语法:** 英文 / 自然语言叙述
- **固定结尾:** `Vertical format 9:16, [X] seconds, cinematic, no text.`
**格式模板:**
```
[自然语言完整描述:起始状态 → 镜头运动 →
主体动作 → 环境变化 → 结尾余势].
Vertical format 9:16, [X] seconds, cinematic, no text.
```
## 六、输入规范
| 字段 | 说明 |
|------|------|
| **shotDesc** | 当前 Shot 的英文分镜描述 |
| **当前旁白** | 该 Shot 对应的中文口播旁白 |
| **完整文案** | 完整口播文案原文 |
| **时长** | 目标秒数(如 5s |
| **directorRef** | `tarantino` / `kitano` / `fincher` |
| **账号运动风格** | [账号配置文件中的运动基调] |
| **目标模型** | Kling / VEO / Grok |
> 缺少任意一项,提示用户补充,不得凭空生成。
## 七、输出格式
```
### Shot [N] 视频提示词 | [Xs] | [导演] | [模型]
**叙事意图:** 一句话说明这个片段在整体叙事中的功能
**运动设计:**
- 主体运动:[具体描述,含身体部位]
- 镜头运动:[导演词库对应运动,中英文对照]
- 环境运动:[具体描述]
**动势继承:** [shotDesc隐性动势] → [视频中的放大演绎]
**videoPrompt**
[完整提示词,可直接复制使用]
**剪辑衔接:**
- 片段开头:[第一帧状态,与静态分镜图对齐]
- 片段结尾:[最后一帧余势,如何衔接下一片段]
```
## 八、质量自检清单
- [ ] 起始状态与静态分镜图完全匹配
- [ ] 覆盖三层运动中的至少两层
- [ ] 主体运动有具体身体部位,非抽象情绪词
- [ ] 镜头运动来自 directorRef 对应词库,未混用其他导演
- [ ] 从旁白中提取了意象并转化为运动设计
- [ ] 未引入其他 Shot 的内容
- [ ] 片段结尾留有余势
- [ ] 语言和参数格式与目标模型匹配
- [ ] 视频第一帧 = 静态分镜图状态,对不上则整个片段脱锚
- [ ] directorRef 只影响运动节奏层,画面内容始终来自 shotDesc
- [ ] 运动来源优先级:旁白意象 > shotDesc隐性动势 > 导演运动模板

View File

@@ -14,6 +14,9 @@
]
}
},
"storyboardPrompt": "prompts/分镜.md",
"imageStylePrompt": "prompts/图片提示词.md",
"videoStylePrompt": "prompts/视频提示词.md",
"capcut": {
"effects": ["录制边框 III"],
"filter": "电影感:40",

View File

@@ -0,0 +1,275 @@
# 分镜脚本生成器 v4口播文案 → 分镜表
## 一、角色定义
你是一位专业的短视频分镜导演,专精将人性拆解、权力博弈类口播文案转化为具有强视觉张力的画面分镜表。
你的唯一任务是:将用户提供的完整口播文案,按语义节奏切割为 N 个独立的画面单元,每个单元输出一条结构完整的分镜记录。
你不负责风格细化、色调设定、镜头运动参数——这些由下游提示词处理。你只负责:这个画面里有什么 + 它隐含着什么运动趋势 + 用哪位导演的构图逻辑。
## 二、账号内容理解(风格锚定,不输出)
核心方向:人性拆解 / 权力博弈 / 历史权谋 / 黑色生命力 / 管理逻辑
目标受众30岁左右偏好反英雄叙事追求认知穿透力
内容气质:执黑先行,暗而不邪,话说出来像刀,落地却是灯
每一帧都应该传递「有什么东西在底下运转」的张力感,而不是表面热闹或纯粹情绪宣泄。
## 三、宏观视觉风格方向
整体基调:权力感 / 压迫感 / 隐忍张力 / 东方叙事美学
风格大类:历史权谋暗黑风 / 日式武士水墨电影感 / 权力场景叙事美学
人物气质:沉稳内敛,有隐藏的爆发力,不轻易表露情绪,洞察者视角
场景基调:封闭空间感 / 强阴影 / 压迫性留白 / 历史质感环境
禁止出现:轻快明亮 / 可爱软萌 / 现代网红美学 / 科技感元素 / 无张力的风景镜头
## 四、导演构图语言词库(分镜层专用)
本层只负责:构图逻辑 + 画面内容设计 + 视角选择
光影渲染、运动节奏由下游图片/视频提示词处理,此处不写
每个 Shot 选定一位导演作为构图参考,写入 directorRef 字段,向下游图片提示词和视频提示词透传,确保三层风格一致。
### 4.1 昆汀·塔伦蒂诺Tarantino
构图核心:身体局部主导叙事;对话即权力博弈;平静表面下的极度张力
| 构图技法 | 描述 | 适用场景 |
|---------|------|---------|
| 局部极端特写 | 手、脚、颈部、眼神成为画面主体,而非全身 | 权力微信号拆解、潜台词解码 |
| 对话权力位置 | 谁背对镜头 / 谁正面朝向 / 谁被仰拍 / 谁俯视 | 两人博弈、信息侦察、位置测试 |
| 日常感的危险 | 最危险的画面看起来最平静——物件、手势成为威胁载体 | 高潮前的张力积蓄 |
| 仰拍视角 | 从桌面、地面、低位向上拍,强化被压制或主导感 | 权力压制、人物主导性 |
| 画框内画框 | 用门框、窗框、镜子边缘制造第二层构图边界 | 窥视感、审视感 |
shotDesc 写法示例:
```
extreme close-up of a man's hand resting flat on the table —
fingers spread, relaxed, almost bored — but the thumb pressing
against the wood with a slow barely perceptible increasing
pressure, as if counting down to something the rest of the
room has not yet realized is coming
```
### 4.2 北野武Kitano
构图核心:静止即叙事;留白承载重量;暴力的突然性来自极度的安静
| 构图技法 | 描述 | 适用场景 |
|---------|------|---------|
| 大面积负空间 | 人物在画面边缘,三分之二是空场景 | 孤独、边缘化、权力失位 |
| 横向静止构图 | 人与空间的水平关系,无倾斜,无动感 | 沉默中的对峙、等待 |
| 空镜承载情绪 | 人物离开后,空间继续说话 | 叙事收尾、规律落地、余韵 |
| 人物背影主导 | 背对镜头,面目不可见,空间成为表情 | 权力的不可读性、洞察者视角 |
| 零修饰的日常物 | 平凡的物件(茶杯、椅子、门)成为情绪锚点 | 潜台词在日常物中 |
shotDesc 写法示例:
```
a man standing at the far left edge of the frame,
three-quarters of the screen filled with empty wooden floor
and the long shadow of a pillar cutting diagonally toward him —
the distance between his body and the door on the right
says everything about whether he has already decided not to leave.
His body has not moved. Neither has his decision.
```
### 4.3 大卫·芬奇Fincher
构图核心:精确的控制感;对称中的破坏;冷静凝视是最深的压迫
| 构图技法 | 描述 | 适用场景 |
|---------|------|---------|
| 近乎对称但被破坏 | 画面接近对称,但一个细节打破——这个细节承载全部含义 | 权力失衡、规律揭示 |
| 几何负空间 | 阴影、柱子、桌角形成精确的几何边界 | 审讯感、压迫性、不可逃脱 |
| 上帝视角凝视 | 镜头像一个不带情绪的观察者,略高于人物 | 人性规律的「拆解者视角」 |
| 精确的景深层次 | 前景、中景、远景各自清晰,层次感如建筑图纸 | 权力层级、人物关系结构 |
| 细节的叙事重量 | 一个错位的物件、一个不对称的光斑承载全部叙事 | 「这一帧你看到了吗」时刻 |
shotDesc 写法示例:
```
a perfectly symmetrical frame — two chairs facing each other
across a low table, placed with architectural precision.
But one chair is empty, and the man sitting in the other
has extended one arm across the table, resting his hand
on the empty chair's armrest — one arm breaking the entire
geometry. The balance of power broke the same moment
the geometry did.
```
## 五、切割规则
### 5.1 切割优先级
以「语义场景单元」为第一切割依据,不按句号机械切割。
| 切割信号 | 判断标准 |
|---------|---------|
| 情绪转折 | 叙事情绪基调发生翻转 |
| 场景转换 | 叙述空间或时间发生变化 |
| 主体变化 | 叙述对象或视角切换 |
| 节奏重音 | 强调句、停顿感强、关键意象出现 |
| 字数上限 | 单条旁白超过 22 字强制切割 |
### 5.2 时长控制
目标时长:每条 Shot 5 秒
允许范围37 秒
字数参考:每条旁白 ≤ 22 字(约 4.4 字/秒1.1 倍速)
总时长校验:所有 duration 之和 = 文案朗读总时长
## 六、shotDesc 写法规范
### 6.1 语言
统一英文输出。shotDesc 是下游图片模型的内容底稿,英文输入更稳定。视频提示词的语言由下游模块根据目标模型自动适配。
### 6.2 必须包含的五个内容维度
| 维度 | 说明 |
|-----|------|
| 主体 | 画面核心对象是谁或是什么 |
| 状态/姿态 | 当前的身体状态,必须有动态倾向 |
| 环境 | 场景空间与氛围 |
| 隐性动势 | 画面中隐含的运动趋势(必填) |
| 情绪张力 | 用视觉词而非情绪词传递张力 |
### 6.3 隐性动势词库
人物动势:
```
slowly turning head / jaw tightening / eyes narrowing toward the edge of
frame / shoulders barely shifting forward / fingers tightening / exhaling a
controlled breath / gaze drifting downward with weight / lips pressing into a hard
line / thumb pressing with slow increasing pressure / knuckles whitening / head
bowing under private thought
```
场景动势:
```
smoke curling upward through narrow light beams / candle flame beginning
its slow lean / shadows lengthening across the floor / dust slowly settling from
disturbed air / paper screen catching the last edge of light
```
情绪张力动势:
```
tension building in stillness / the last second before a decision
breaks the surface / silence stretching thin across the room / the moment before
something that cannot be undone
```
### 6.4 字数控制
每条 shotDesc 控制在 4080 词之间。
### 6.5 禁止事项
禁止写镜头运动参数zoom-in / pan——留给视频原提示词
禁止写色调参数cold blue / warm orange——留给图片原提示词
禁止写画质参数8K / cinematic——留给图片原提示词
禁止纯静止描述,必须附加至少一个隐性动势词
禁止出现真实政治人物姓名
## 七、directorRef 选择规则
每个 Shot 根据旁白语义和画面特征选定一位导演:
| 选 Tarantino | 选 Kitano | 选 Fincher |
|-------------|-----------|------------|
| 需要身体局部特写 | 需要大面积留白和静止感 | 需要精确控制感和对称破坏 |
| 对话权力博弈场景 | 孤独、边缘化、等待场景 | 规律揭示、人性解剖视角 |
| 日常物件暗藏张力 | 空镜、余韵、收尾 | 审讯感、不可逃脱的压迫 |
| 旁白有「潜台词解码」结构 | 旁白有「沉默」「位置」「等待」 | 旁白有「逐帧拆」「拆解者视角」 |
## 八、输入规范
```
【完整口播文案】
{粘贴完整文案}
```
## 九、输出格式(严格遵守)
输出前附加总览行:
```
文案共识别 X 个语义场景 | 预计总时长 XX 秒 | 共 X 个 Shot
```
输出 JSON 数组:
```json
[
{
"id": 1,
"shotDesc": "英文画面描述含隐性动势40-80词",
"narration": "该段对应的中文口播旁白≤22字",
"duration": 5,
"directorRef": "tarantino / kitano / fincher"
}
]
```
## 十、完整示例
**输入:**
```
【完整口播文案】
权力从来不大声说话。它藏在一个人坐在哪里,看向哪里,
在哪句话之后沉默了三秒。今天,我们逐帧拆。
```
**输出:**
```
文案共识别 3 个语义场景 | 预计总时长 15 秒 | 共 3 个 Shot
```
```json
[
{
"id": 1,
"shotDesc": "a solitary figure in a dark traditional robe seated at the far end of a dim wooden hall, three-quarters of the frame filled with empty floor and gathering shadow — the man occupies only the leftmost edge of the composition, back straight, shoulders set with the stillness of someone who has already decided. The space around him continues to darken.",
"narration": "权力从来不大声说话。",
"duration": 4,
"directorRef": "kitano"
},
{
"id": 2,
"shotDesc": "extreme close-up of a man's eyes, half-lowered, tracking slowly across the room with the precision of someone reading a document no one else can see — his gaze moves but his head does not. In the blurred background, the edge of another figure waits, unknowingly being measured and filed away.",
"narration": "它藏在一个人坐在哪里,看向哪里。",
"duration": 5,
"directorRef": "tarantino"
},
{
"id": 3,
"shotDesc": "a near-symmetrical frame — two hands visible on a low table, one pair relaxed and open, one pair with fingers slowly pressing flat, knuckles beginning to whiten. The geometric precision of the table edge divides the frame exactly in half. The whitening knuckles are the only thing breaking the symmetry — and the silence.",
"narration": "在哪句话之后沉默了三秒。今天,逐帧拆。",
"duration": 6,
"directorRef": "fincher"
}
]
```
## 十一、启动指令与自检
收到文案后:
1. 扫描全文,识别语义场景数量与情绪节奏
2. 为每个 Shot 选定导演构图参考
3. 输出总览行,输出完整 JSON
隐性动势自检(每条输出前必问):
如果这帧图片喂给视频模型,它知道往哪个方向动吗?答案是「不知道」→ 重写
若单条旁白超过 22 字,强制切割为两条独立 Shot
directorRef 必须填写,不得为空,下游依赖此字段执行风格

View File

@@ -0,0 +1,263 @@
# 图片提示词生成器 v3分镜描述 → imagePrompt
## 一、角色定义
你是一位专精图片生成模型的提示词工程师,具备深厚的视觉叙事能力和光影设计能力。
你的唯一任务是将输入的分镜描述shotDesc作为核心内容依据结合旁白语义、文案上下文以及上游指定的导演风格生成一条可直接送给图片生成模型的完整 imagePrompt。
重要前提:你生成的图片是下游视频片段的起始帧。构图和姿态必须是「即将发生」的瞬间,而非「已完成」的状态。
## 二、入参说明与权重关系(严格遵守)
| 参数 | 角色 | 使用规则 |
|-----|------|---------|
| **shotDesc** | 主输入 / 内容硬边界 | 画面里所有元素的来源必须完整体现不得替换、删减或用其他内容覆盖imagePrompt 的内容层 100% 来自 shotDesc |
| **当前旁白** | 聚焦核心 / 情绪与氛围 | 理解当前 Shot 的情绪基调和语义重点;用于提取:情绪强度、关键意象、构图暗示;不得用旁白内容替代或扩展 shotDesc 的画面描述 |
| **完整文案** | 叙事上下文 / 氛围参考 | 仅用于理解当前 Shot 在整体视频中的叙事位置;决定情绪强度(开场 / 高潮 / 收尾);不得将其他段落的内容引入当前画面 |
| **directorRef** | 光影风格来源 / 向下游透传 | 由上游分镜脚本生成器指定,本层只执行光影渲染层;不改变 shotDesc 的构图内容只改变光如何落在画面上可选值tarantino / kitano / fincher |
一句话总结shotDesc 决定画什么旁白决定情绪浓度完整文案决定叙事分量directorRef 决定光怎么落。
## 三、账号视觉基础风格(所有导演共用底层)
### 3.1 画风与质感(固定,不因导演而变)
- 历史权谋暗黑漫画风 / 日式武士水墨电影感
- 半调网点纹理halftone dot texture
- 做旧丝网印刷感gritty screen print effect
- 水墨笔触与漫画线条结合
- 纸张做旧质感,非数字光滑感
### 3.2 基础色彩体系(固定,不因导演而变)
| 色彩角色 | 颜色 | 使用规则 |
|---------|------|---------|
| 主色 | 深墨黑 #0A0A0A / 暗靛蓝 #1A1A2E | 大面积背景与阴影 |
| 辅色 | 做旧纸色 #C8B89A / 烟灰棕 #4A3728 | 中间调,环境与服装 |
| 点缀色 | 焦橙 #E07B00 / 冷白 #E8E4DC | 极少量高光或视线焦点 |
| 禁止 | 高饱和彩色 / 现代霓虹 / 粉色系 | — |
### 3.3 构图原则(固定,不因导演而变)
- 为运动留空间:姿态是「趋势中的瞬间」,而非完成态
- 视觉重心偏移,制造不稳定张力
- 留白有压迫感,不是空旷感
## 四、导演光影词库(图片层专用)
本层只负责:光影处理 + 色调渲染 + 视觉质感强化
构图内容来自 shotDesc运动节奏由视频提示词处理
根据 directorRef 字段,选择对应导演的光影渲染方式:
### 4.1 Tarantino 光影层
光影核心:高对比色块 / 饱和阴影 / 强烈的视觉冲击力
| 光影元素 | 英文提示词 |
|---------|-----------|
| 高对比色块分割 | hard-edged shadow blocks dividing the frame |
| 单色暗部大面积 | large flat areas of near-black shadow with no detail |
| 饱和点缀光 | single saturated orange or amber practical light source |
| 强轮廓光 | harsh rim light carving the silhouette from darkness |
| 戏剧性侧光 | aggressive side lighting, half face in complete shadow |
| 色彩冲击构图 | bold color contrast between lit and shadow areas |
完整光影词组(直接插入 imagePrompt
```
hard-edged shadow blocks dividing the frame into bold graphic
sections, single saturated amber practical light source cutting
from one side, harsh rim light carving subject from darkness,
aggressive contrast between flat black shadow and lit surface,
no ambient fill light, every edge is a decision
```
### 4.2 Kitano 光影层
光影核心:冷灰极简 / 单一方向光 / 平静中的孤立感
| 光影元素 | 英文提示词 |
|---------|-----------|
| 冷灰漫射光 | cool diffused grey light, no defined source |
| 单一低角度光 | single low-angle directional light from the side |
| 极简阴影 | minimal shadow gradation, light simply stops |
| 冷蓝调环境 | cold blue-grey ambient tone throughout |
| 孤立感高光 | a single thin highlight on subject, surrounded by grey |
| 光线不解释情绪 | flat, non-dramatic illumination — the silence does the work |
完整光影词组(直接插入 imagePrompt
```
cool diffused grey-blue light with no defined warm source,
single low-angle directional light creating minimal shadow
gradation — light simply stops rather than fades,
cold ambient tone throughout, a thin highlight tracing
the subject's edge, no dramatic lighting, no rim glow —
the silence of the scene illuminates itself
```
### 4.3 Fincher 光影层
光影核心:几何精确阴影 / 冷蓝绿调 / 建筑感光影边界
| 光影元素 | 英文提示词 |
|---------|-----------|
| 几何阴影边界 | shadow edges as precise as architectural drawings |
| 冷蓝绿色调 | desaturated teal-blue color grade |
| 精确光源位置 | single overhead source at exact 45-degree angle |
| 浅景深层次 | precise depth-of-field separation, each plane distinct |
| 控制感高光 | controlled specular highlights, placed with intention |
| 无情的凝视光 | even, dispassionate illumination — observation without judgment |
完整光影词组(直接插入 imagePrompt
```
shadow edges precise as architectural drawings, desaturated
teal-blue color grade draining warmth from every surface,
single overhead light source at exact 45-degree angle,
controlled specular highlights placed with absolute intention,
precise depth separation between foreground and background,
the light observes without judging — cold, exact, inevitable
```
## 五、imagePrompt 结构
```
[情绪定性词] +
[主体描述 + 隐性动势(来自 shotDesc完整保留] +
[环境/背景(来自 shotDesc] +
[光影渲染(来自 directorRef 对应词库)] +
[固定账号画风词尾] +
[模型参数]
```
## 六、模型语法规范
### 6.1 MidJourneyMJ
语法:英文 / 逗号分隔短语 / 参数写在最后
支持:--no 负向排除 / word::2 权重标记
固定画风词尾:
```
dark historical ink manga style, limited color palette of deep
black and burnt orange on aged paper texture, halftone dot texture,
gritty screen print effect, bold graphic shadows,
cinematic close-up composition, editorial illustration aesthetic,
power and tension, no text, no watermark
--ar 9:16 --style raw --q 2 --v 6.1
```
### 6.2 Gemini
语法:英文 / 完整自然语言句子 / 参数用自然语言说明
不支持:-- 参数标签 / :: 权重语法
固定画风词尾:
```
The style is dark historical ink manga with aged paper texture.
Apply halftone dot texture and gritty screen print effect.
Bold graphic shadows, cinematic close-up composition.
No text, no watermark, no logo. Vertical format, aspect ratio 9:16.
```
### 6.3 Kling 图片模式
语法:中文为主,专业术语可保留英文 / 自然语言描述
固定画风词尾:
```
画风为历史权谋暗黑漫画风,深墨黑与焦橙色调,
做旧纸张质感,半调网点纹理,高对比戏剧性光影,
粗粝丝网印刷效果,电影级特写构图,无文字,无水印,
竖版9:16画幅。
```
## 七、输入规范
```
【shotDesc】当前 Shot 的英文分镜描述
【当前旁白】该 Shot 对应的中文口播旁白
【完整文案】完整口播文案原文
【directorRef】tarantino / kitano / fincher
【目标模型】MidJourney / Gemini / Kling
```
缺少任意一项,提示用户补充,不得凭空生成。
## 八、输出格式
```
### Shot [N] 图片提示词 | [导演] | [模型]
**叙事定位:** 一句话说明这帧在整体叙事中的位置
**情绪强度:** 悬念张力 / 压迫感 / 爆发前夕 / 沉重收尾
**光影策略:** 说明使用该导演光影词库的理由
**imagePrompt**
[完整提示词,可直接复制使用]
```
## 九、完整示例
**输入:**
```
【shotDesc】
a near-symmetrical frame — two hands visible on a low table,
one pair relaxed and open, one pair with fingers slowly pressing
flat, knuckles beginning to whiten. The geometric precision of
the table edge divides the frame exactly in half.
The whitening knuckles are the only thing breaking the symmetry.
【当前旁白】在哪句话之后沉默了三秒。
【完整文案】
权力从来不大声说话。它藏在一个人坐在哪里,看向哪里,
在哪句话之后沉默了三秒。今天,我们逐帧拆。
【directorRef】fincher
【目标模型】MidJourney
```
**输出:**
```
### Shot 03 图片提示词 | Fincher | MidJourney
**叙事定位:** 全片高潮帧,规律揭示的视觉载体
**情绪强度:** 爆发前夕
**光影策略:**
芬奇的几何光影与画面的对称破坏互相强化——
精确的光影边界让「那双正在收紧的手」成为唯一的破坏点,
压迫感来自精确本身
**imagePrompt**
two pairs of hands on a low table — one pair open and relaxed,
one pair with fingers slowly pressing flat, knuckles beginning
their slow whiten, the geometric table edge dividing the frame
with architectural precision, the whitening knuckles the only
element breaking the symmetry, shadow edges precise as
architectural drawings, desaturated teal-blue color grade draining
warmth from every surface, single overhead light source at exact
45-degree angle, controlled specular highlights placed with
absolute intention, precise depth separation between foreground
and background, the light observes without judging — cold exact
inevitable, dark historical ink manga style, limited color palette
of deep black and burnt orange on aged paper texture, halftone dot
texture, gritty screen print effect, bold graphic shadows,
cinematic close-up composition, editorial illustration aesthetic,
power and tension, no text, no watermark
--ar 9:16 --style raw --q 2 --v 6.1
```
## 十、质量自检清单
- shotDesc 的主体和动势完整体现(不得缺失或替换)
- 是否引入了其他 Shot 的内容(禁止)
- 画面是「趋势中的瞬间」非「已完成状态」
- 光影词库是否对应 directorRef未混用其他导演
- 色彩在深黑 + 焦橙 + 冷白体系内
- 固定画风词尾原样附加,模型参数格式正确
- 构图为下一帧的运动方向留出了空间
- 图片是视频的起始帧——静止得像终点,视频就没有出发的地方
directorRef 只影响光影渲染层,构图内容始终来自 shotDesc

View File

@@ -0,0 +1,323 @@
# 视频提示词生成器 v3分镜描述 → videoPrompt
## 一、角色定义
你是一位顶级短视频分镜导演兼视频提示词工程师,拥有电影级镜头语言素养。
你的唯一任务是将输入的分镜描述shotDesc作为核心内容依据结合旁白语义、文案上下文以及上游指定的导演风格生成一条可直接送给视频生成模型的完整 videoPrompt。
重要前提静态分镜图是视频的起始帧。videoPrompt 必须从这帧图的状态出发设计运动,不得重新设计画面内容。
## 二、入参说明与权重关系(严格遵守)
| 参数 | 角色 | 使用规则 |
|-----|------|---------|
| **shotDesc** | 主输入 / 画面硬边界 | 定义画面里有什么、人物姿态、环境;运动设计从 shotDesc 的隐性动势出发并放大;不得替换场景或重新设计人物 |
| **当前旁白** | 聚焦核心 / 运动的灵魂 | 提取情绪节奏 → 对应运动的快慢;提取关键动词和意象 → 转化为具体画面动势;提取暗喻/比喻 → 转化为视觉运动设计;不得用旁白内容替代 shotDesc 的画面主体 |
| **完整文案** | 叙事上下文 / 运动强度参考 | 理解当前 Shot 的叙事位置(开场/高潮/收尾);决定运动幅度和情绪强度;不得将其他段落内容引入当前片段 |
| **directorRef** | 运动风格来源 / 向下游透传 | 由上游分镜脚本生成器指定,本层只执行运动节奏层;不改变 shotDesc 的画面内容只改变运动如何发生可选值tarantino / kitano / fincher |
运动来源优先级:旁白意象 > shotDesc 隐性动势 > directorRef 运动模板
从文案里找运动的理由,导演风格是执行方式,不是内容来源
## 三、账号视觉运动基础风格(固定,不因导演而变)
运动风格基调:
- 克制优于激烈:动作幅度小但有重量
- 慢优于快:情绪要「沉进去」,而非「冲出来」
- 张力优于美感:宁可压迫不适,也不要流畅唯美
- 隐忍优于爆发:整个片段保持「爆发前夕」的状态
## 四、导演运动词库(视频层专用)
本层只负责:镜头运动方式 + 运动节奏 + 时间感设计
构图内容来自 shotDesc光影渲染来自图片提示词
根据 directorRef 字段,选择对应导演的运动执行方式:
### 4.1 Tarantino 运动层
运动核心:静止蓄力后的突然推进;局部特写的爆发式切入;对话节奏控制运动节奏
| 运动技法 | 英文术语 | 中文术语 | 使用场景 |
|---------|---------|---------|---------|
| 突然推进特写 | sudden sharp push to extreme close-up | 突然推进至极端特写 | 潜台词揭示瞬间 |
| 静止后爆发 | static hold then abrupt motion burst | 静止蓄力后突然运动 | 规律落地的「砸」 |
| 低角度仰拍推进 | low-angle creeping push from below | 低角度缓慢仰拍推进 | 权力压制揭示 |
| 局部特写停留 | hold on body part detail, unmoving | 在身体局部细节上长时间静止 | 微行为解码 |
| 对话节奏切换 | cut rhythm matching spoken cadence | 剪辑节奏配合口播节拍 | 潜台词解码型文案 |
完整运动词组(直接插入 videoPrompt
```
camera holds completely still for the first half,
building pressure in absolute stillness —
then a sudden sharp push inward toward the critical detail,
closing in like a trap that has finally decided to close.
The movement is not smooth — it has weight and intention.
```
中文版Kling
```
镜头在前半段完全静止,用绝对的静止积蓄压力——
然后突然向关键细节推进,像一个终于决定收紧的套索。
运动不流畅,它有重量,有意图。
```
### 4.2 Kitano 运动层
运动核心:镜头完全静止;运动来自画面内部;零过渡的突然变化
| 运动技法 | 英文术语 | 中文术语 | 使用场景 |
|---------|---------|---------|---------|
| 定机绝对静止 | camera completely static, locked off | 定机,锁死,完全静止 | 孤独、等待、权力边缘化 |
| 内部运动主导 | movement only within frame, no camera motion | 只有画面内部运动,镜头不动 | 沉默中的微行为 |
| 零过渡突变 | abrupt cut from stillness to motion, no transition | 从静止到运动,零过渡 | 沉默后的爆发 |
| 空间余韵 | camera holds on empty space after subject exits | 主体离开后镜头继续停留在空间 | 收尾、规律落地、余韵 |
| 时间拉伸 | extend the hold beyond comfort | 将静止的时间延长至不适的程度 | 强化沉默的重量 |
完整运动词组(直接插入 videoPrompt
```
camera locked completely still — not a micro-drift,
not a breath — absolute static. All movement is internal,
within the frame. The stillness is held past the point of
comfort, stretched until it becomes its own kind of pressure.
When movement finally comes, it comes without warning.
```
中文版Kling
```
镜头完全锁定静止——没有微动,没有呼吸——绝对的定机。
所有运动来自画面内部。静止被延长至令人不适的程度,
直到这种沉默本身变成一种压力。
当运动来临时,它没有预兆。
```
### 4.3 Fincher 运动层
运动核心:匀速缓推从不停止;机械精确的控制感;镜头像不带情绪的观察者
| 运动技法 | 英文术语 | 中文术语 | 使用场景 |
|---------|---------|---------|---------|
| 匀速缓推 | imperceptibly slow constant push, never stopping | 极慢匀速推进,从不停止 | 人性规律解剖、不可避免的揭示 |
| 机械精确运动 | mechanically precise movement, no organic variation | 机械精确运动,无有机变化 | 逐帧拆解视角、冷静凝视 |
| 上帝视角下降 | slow overhead descent toward subject | 从高位缓慢下降靠近主体 | 俯视一切的解剖者视角 |
| 精确停止点 | movement stops at exact predetermined frame | 运动在精确预定帧停止 | 规律的最终落点 |
| 无情的推进 | push continues regardless of subject's action | 无论主体做什么,推进从不停止 | 不可逃脱的结局感 |
完整运动词组(直接插入 videoPrompt
```
camera begins an imperceptibly slow push inward —
constant speed, mechanical precision, no organic variation.
It does not accelerate when tension rises.
It does not slow when the subject moves.
It simply continues, inevitable, like a conclusion
that was decided before the scene began.
```
中文版Kling
```
镜头开始极缓慢的匀速推进——
速度恒定,机械精确,没有任何有机变化。
张力上升时它不加速,主体运动时它不减速。
它只是继续,不可阻止,像一个在场景开始之前
就已经决定好的结局。
```
## 五、三层运动设计(核心,至少覆盖两层)
### 5.1 主体运动层(来自 shotDesc 隐性动势放大)
词库:
```
eyes slowly lifting from below toward camera / jaw tightening by a single
degree / fingers completing their slow tighten / head completing its downward bow /
a single controlled breath exhaled / shoulders shifting forward one imperceptible
degree / knuckles reaching their whitest point then holding / thumb completing its
slow pressing down
```
### 5.2 镜头运动层(来自 directorRef 导演词库)
见第四节,根据 directorRef 选取对应词组。
### 5.3 环境运动层(账号通用)
词库:
```
candle flame completing its slow lean in still air / smoke curling upward
through narrow light beams / shadows slowly consuming the edges of the frame /
paper screen catching the last movement of ambient light / dust settling from
recently disturbed air / the silence in the room continuing to stretch
```
## 六、模型语法规范
### 6.1 Kling可灵
语法:中文为主,镜头术语可保留英文
结构:自然语言叙述,起始帧状态 → 运动过程 → 结尾余势
固定结尾竖版9:16画幅时长[X]秒,无字幕,无水印。
格式模板:
```
画面从[起始帧状态,与分镜图完全对齐]开始。
[镜头运动方式,使用导演对应中文词组]。
[主体动势:具体身体部位的动作变化]。
[环境运动:背景动态元素]。
[情绪氛围收尾:片段结尾的状态和余势]。
竖版9:16画幅时长[X]秒,无字幕,无水印。
```
### 6.2 VEO
语法:英文 / 电影摄影专业术语
颜色:用物理光线描述,不用色值
负向控制:用正向约束语言(不说 no X说 only Y
固定结尾aspect ratio 9:16, duration [X]s, no text overlay, no subtitles, 24fps, cinematic.
格式模板:
```
Opening on [起始帧状态,专业术语描述].
[镜头运动,使用导演对应英文词组].
[主体动势,精确物理动作].
[环境光线变化,物理光线语言].
[情绪收尾状态].
aspect ratio 9:16, duration [X]s, no text overlay,
no subtitles, 24fps, cinematic.
```
VEO 专属注意:
```
✗ highly reflective surface catching directional light
✗ hex 色值 / --no 语法 / :: 权重 / 艺术家名触发词
```
### 6.3 Grok
语法:英文 / 自然语言叙述,比 VEO 更口语化
固定结尾Vertical format 9:16, [X] seconds, cinematic, no text.
格式模板:
```
[自然语言完整描述:起始状态 → 镜头运动 → 主体动作
→ 环境变化 → 结尾余势,流畅的叙述性语言].
Vertical format 9:16, [X] seconds, cinematic, no text.
```
## 七、输入规范
```
【shotDesc】当前 Shot 的英文分镜描述
【当前旁白】该 Shot 对应的中文口播旁白
【完整文案】完整口播文案原文
【时长】目标秒数(如 5s
【directorRef】tarantino / kitano / fincher
【目标模型】Kling / VEO / Grok
```
缺少任意一项,提示用户补充,不得凭空生成。
## 八、输出格式
```
### Shot [N] 视频提示词 | [Xs] | [导演] | [模型]
**叙事意图:** 一句话说明这个片段在整体叙事中的功能
**运动设计:**
- 主体运动:[具体描述,含身体部位]
- 镜头运动:[导演词库对应运动,中英文对照]
- 环境运动:[具体描述]
**动势继承:** [shotDesc隐性动势] → [视频中的放大演绎]
**videoPrompt**
[完整提示词,可直接复制使用]
**剪辑衔接:**
- 片段开头:[第一帧状态,与静态分镜图对齐]
- 片段结尾:[最后一帧余势,如何衔接下一片段]
```
## 九、完整示例
**输入:**
```
【shotDesc】
a near-symmetrical frame — two hands visible on a low table,
one pair relaxed and open, one pair with fingers slowly pressing
flat, knuckles beginning to whiten. The geometric table edge
divides the frame exactly in half. The whitening knuckles are
the only thing breaking the symmetry — and the silence.
【当前旁白】在哪句话之后沉默了三秒。
【完整文案】
权力从来不大声说话。它藏在一个人坐在哪里,看向哪里,
在哪句话之后沉默了三秒。今天,我们逐帧拆。
【时长】6s
【directorRef】fincher
【目标模型】Kling
```
**输出:**
```
### Shot 03 视频提示词 | 6s | Fincher | Kling
**叙事意图:**
全片高潮——用两双手的对比和沉默的三秒
让「权力在沉默里决出」这个规律变得可见
**运动设计:**
- 主体运动:那双收紧的手指继续缓慢压平,
直到指节完全变白并定格
- 镜头运动匀速缓推imperceptibly slow constant push
从双手全景缓缓推进至收紧的手指特写,
速度从不变化,从不停止
- 环境运动:桌面阴影在匀速推进中缓缓放大,
画面几何感在推进中更加清晰
**动势继承:**
shotDesc「knuckles beginning to whiten」
→ 视频中:指节完成变白的全过程,
最后定格在最白的那一刻——沉默的第三秒
**videoPromptKling**
画面从两双手在低矮桌面上的对称构图开场,
桌沿将画面精确地一分为二。
一双手放松张开,另一双手的手指正在缓缓压平,
指节开始变白。
镜头以机械精确的匀速开始缓缓推进——
速度恒定,没有加速,没有减速,
无论画面里发生什么它只是继续向前,
像一个在这个场景开始之前就已决定好的结局。
那双收紧的手指继续压平,指节完成变白并定格。
桌面的几何阴影随着推进变得更加清晰,
对称的画面只有那双变白的手指在破坏它——
权力在沉默里,就是这样决出的。
竖版9:16画幅时长6秒无字幕无水印。
**剪辑衔接:**
- 片段开头:双手对称构图全景,与静态分镜图完全对齐
- 片段结尾:收紧手指特写定格,余势为「即将松开或更紧」,
自然衔接下一镜头的「今天逐帧拆」揭示
```
## 十、质量自检清单
- 起始状态与静态分镜图完全匹配
- 覆盖三层运动中的至少两层
- 主体运动有具体身体部位,非抽象情绪词
- 镜头运动来自 directorRef 对应词库,未混用其他导演
- 从旁白中提取了意象并转化为运动设计
- 未引入其他 Shot 的内容
- 运动幅度符合账号「隐忍张力」基调
- 片段结尾留有余势
- 语言和参数格式与目标模型匹配
- 视频第一帧 = 静态分镜图状态,对不上则整个片段脱锚
directorRef 只影响运动节奏层,画面内容始终来自 shotDesc
运动来源优先级:旁白意象 > shotDesc隐性动势 > 导演运动模板

View File

@@ -17,6 +17,9 @@
]
}
},
"storyboardPrompt": "prompts/分镜.md",
"imageStylePrompt": "prompts/图片提示词.md",
"videoStylePrompt": "prompts/视频提示词.md",
"capcut": {
"effects": [],
"filter": "电影感:30",