feat(skills): 完善视频生产 pipeline 及新增健身跟练账号

- SKILL.md: 新增工作流阶段定义、质量卡点、分镜规则
- manifest-schema.md: 补充完整字段规范及类型定义
- phase-tts.js: 优化 TTS 合成长逻辑,添加进度追踪
- capcut-tracks.js: 扩展轨道构建能力,支持更多元素类型
- capcut-timeline.js: 改进时间线生成,支持淡入淡出
- capcut_assemble.js: 新增 assemble 阶段完整实现
- cmd-init.js: 完善 init 命令逻辑
- qwen-tts.js: 调整超时配置
- accounts/禁忌帝王学: 更新拆分/图像/台词提示词
- accounts/健身跟练: 新增账号含 account.json 及全套提示词模板
- 新增 workflow-issues-20260501.md 参考文档

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
lc
2026-05-06 22:53:37 +08:00
parent e6daf7a8d8
commit 6eec0e8889
28 changed files with 2199 additions and 253 deletions

View File

@@ -1,5 +1,5 @@
{ {
"jianyingDraftPath": "C:/Users/45070/AppData/Local/JianyingPro/User Data/Projects/com.lveditor.draft", "jianyingDraftPath": "/Users/lc/Movies/JianyingPro/User Data/Projects/com.lveditor.draft",
"capcutMateDir": "C:/Users/45070/capcut-mate", "capcutMateDir": "C:/Users/45070/capcut-mate",
"capcutMateApiBase": "http://capcut.muyetools.cn/openapi/capcut-mate/v1", "capcutMateApiBase": "http://capcut.muyetools.cn/openapi/capcut-mate/v1",
"imgbbApiKey": "deprecated", "imgbbApiKey": "deprecated",

View File

@@ -78,6 +78,14 @@ node .claude/skills/video-from-script/scripts/get-template-path.js --account <
``` ```
输出示例:`accounts\军事账号\prompts\分镜.md` 输出示例:`accounts\军事账号\prompts\分镜.md`
**时间线核心规则(固化铁律,必须透传给分镜子 Agent**
- 文案是时间轴唯一锚点:总音频时长 = 文案总字数 ÷ 5语速 5字/秒)
- Kling 可灵视频片段固定 6 秒
- **每个 shot 的 TTS 估算(= script字数÷5必须 ≤ 6 秒**
- TTS > 6s → 强制在语义断点处拆分,拆分后每段 script = 语义子句(原句切分),**禁止用完整句重复填充多 shot**
- 合并后 script 拼接 = 原句一字不差
- audioDur > videoDur × 212s的 shot 禁止合并,必须拆分
**子 Agent prompt 必须包含** **子 Agent prompt 必须包含**
1. `模板文件绝对路径:{get-template-path.js 输出的路径,转为绝对路径}`,并指示子 Agent "先 Read 此文件全文,严格按模板规则执行" 1. `模板文件绝对路径:{get-template-path.js 输出的路径,转为绝对路径}`,并指示子 Agent "先 Read 此文件全文,严格按模板规则执行"
2. 用户完整口播文案 2. 用户完整口播文案
@@ -87,10 +95,28 @@ node .claude/skills/video-from-script/scripts/get-template-path.js --account <
**禁止**:主 Agent 不得摘要模板内容传给子 Agent必须让子 Agent 直接读文件。 **禁止**:主 Agent 不得摘要模板内容传给子 Agent必须让子 Agent 直接读文件。
```json ```json
[{"id":1,"shotDesc":"英文画面描述","script":"中文口播文案","duration":5,"directorRef":"tarantino","keyword":"权力"}] [{"id":1,"shotDesc":"英文画面描述","script":"中文口播文案","duration":"TTS估算=字数÷5","directorRef":"fincher"}]
``` ```
**主 Agent 审查**时长合理隐性动势完整视频模式directorRef 已填?视频模式 script 拼接校验通过? **主 Agent 审查(时间线合规优先):**
1. 每个 shot 的 TTS 估算是否 ≤ 6s→ 超过必须打回
2. TTS > 6s 的 shot 是否已正确拆分script 是否为语义子句?
3. 时长合理隐性动势完整视频模式directorRef 已填?
4. 视频模式 script 拼接校验通过(一字不差)?
**分镜规划展示(每次必须输出此表):**
```
文案: [原文]
总字数: N字 | 总音频估算: X.Xs | 视频片段: N个 | 视频模型固定时长: Kling=6s
TTS语速: 1.15x(写死在 qwen-tts.js| 音频调速: 禁止 | 视频适配: 加速/截断
| # | TTS估算 | script字数| ratio估算 | 策略 | 拆分说明 | directorRef |
|---|---------|------------|---------|------|---------|------------|
| 1 | 4.8s | 你只有看清了...24字| 6/17.5=0.34 | ⚠禁止 | TTS>6s需拆 | fincher |
| 2 | 5.2s | 其实这是...26字| 6/27=0.22 | ⚠禁止 | TTS>6s需拆 | fincher |
...
```
ratio = estimatedVideoDuration(6s) / estimatedAudioDuration字数÷5
→ 展示给用户确认。确认后**分镜表锁定为脊骨契约**,下游禁止增减 shot。 → 展示给用户确认。确认后**分镜表锁定为脊骨契约**,下游禁止增减 shot。
@@ -197,7 +223,9 @@ node scripts/pipeline.js confirm --manifest <path> --all
node scripts/pipeline.js status --manifest <path> node scripts/pipeline.js status --manifest <path>
``` ```
**阶段顺序**: `images``upload``videos` `tts` `assemble` **阶段顺序**: `tts``images``upload``videos``assemble`
> **流程变更**TTS 提前到图片生成之前确保音频时长在生图前已知ratio 可控。
**Item 状态**: `pending``generating``done` / `failed` **Item 状态**: `pending``generating``done` / `failed`
@@ -207,6 +235,36 @@ node scripts/pipeline.js status --manifest <path>
每阶段完成后主 Agent 自动校验,不通过则修复后继续。 每阶段完成后主 Agent 自动校验,不通过则修复后继续。
### 分镜规划展示(每次必须输出)
```
文案: [原文]
总字数: N字 | 总音频估算: X.Xs | 视频片段: N个 | 视频模型: Kling=6s
| # | TTS估算 | script内容字数| 拆分说明 | audioDur估算→真实 | ratio |
|---|---------|----------------|---------|------------------|------|
| 1 | 4.8s | 你只有看清了...24字| ✅未超6s | 4.8→17.5s | 0.34⚠ |
| 2 | 5.2s | 其实这是...26字| ✅未超6s | 5.2→27.0s | 0.22⚠ |
...
```
ratio = estimatedVideoDuration / realAudioDuration⚠=需拆分
### 分镜质量卡点(固化)
| 检查项 | 标准 | 不通过则 |
|--------|------|---------|
| 单 shot TTS 估算 | duration ≤ estimatedVideoDurationKling=6s | **强制打回分镜阶段拆分** |
| 长句处理 | TTS>6s → 语义子句拆分script 不重复完整句 | 打回重写 |
| script 内容 | 每个 shot 的 script 是独立语义子句 | 打回重写 |
| 合并校验 | 所有 script 拼接 = 原文一字不差 | 打回重写 |
| ratio 预判 | estimatedVideoDuration / estimatedAudioDuration < 0.9 → 禁止 | 打回分镜重切 |
| 视频模型时长 | estimatedVideoDuration 已填入 manifest | 检查 pipeline init |
**assemble 阶段铁律:**
- 音频以 1.15x 原速导入 CapCut无 speed 字段
- 视频只允许加速speed_up或截断trim
- **禁止慢放slow_down/ 冻结帧freeze/ 音频调速**
### 生图 ### 生图
| 检查项 | 标准 | 不通过 | | 检查项 | 标准 | 不通过 |

View File

@@ -0,0 +1,12 @@
[
{"id": 1, "shotDesc": "A man stands at a crossroad frozen in place, four massive mirrors orbiting him at different angles simultaneously, each reflecting a distorted opposite version of himself — one reading while thinking, one reaching for freedom while chained, one pursuing love without resources, one chasing wealth without action. Fincher cold blue directional light cuts through the scene with architectural shadow lines on the ground. Urban modern fashion.", "script": "99%的人都没意识到的四个致命现象", "duration": 3.8, "directorRef": "fincher"},
{"id": 2, "shotDesc": "A stylish urban figure is split in half by a hard vertical shadow line — left side leans in with an open book, right side rests a hand on a thinking forehead. Twin halves pull in opposite directions, fabric and posture straining against each other. Fincher cold blue practical light, high contrast chiaroscuro.", "script": "不读书却爱思考,不独立却想要自由,没物质却想谈真爱,没执行力却想要发财", "duration": 4.0, "directorRef": "fincher"},
{"id": 3, "shotDesc": "A suited man kicks an old broken clock lying on the ground, its pendulum snapped, yet the clock face still shows hands spinning uncontrollably. The scene freezes mid-kick. Fincher sharp cold practical light, strong shadow, dramatic tension.", "script": "很多事做错了能重来", "duration": 3.0, "directorRef": "fincher"},
{"id": 4, "shotDesc": "A man stands with a black sandbag crushing his shoulders into the ground, four thick chains extending from the bag — each chain wrapped tight around a different part of his body. He strains but cannot break free. Fincher cold blue light, urban background, precise shadow architecture.", "script": "但这四个坑只要踩中一个", "duration": 3.0, "directorRef": "fincher"},
{"id": 5, "shotDesc": "A man running on a treadmill that moves faster and faster beneath him, but the surrounding walls close in with each stride — the space shrinking relentlessly. His expression shifts from determination to desperation. Fincher cold blue overhead light, architectural shadow lines on walls, urban setting.", "script": "你这辈子注定越折腾越穷,越懂事越惨", "duration": 3.4, "directorRef": "fincher"},
{"id": 6, "shotDesc": "A man freezes mid-step on a bridge over dark water, looking down at the vast emptiness below, hands open in the realization. Fincher cold blue natural light, negative space composition, silent tension.", "script": "这不是吓唬你", "duration": 1.0, "directorRef": "fincher"},
{"id": 7, "shotDesc": "A person of any age walks through a dark room full of four suspended iron traps above, each glowing faintly with red warning light overhead. The person looks up, scanning every trap with alert focused eyes. Fincher calculated cold blue overhead practical light, architectural shadows, urban modern fashion.", "script": "一个人无论身处任何年龄阶段都必须提防这四大陷阱", "duration": 4.0, "directorRef": "fincher"},
{"id": 8, "shotDesc": "A person stops at the entrance of four diverging corridors, each corridor is a different trap — fire, ice, void, thorns. The person observes and calculates, then reaches for one. Fincher sharp cold blue light, precise shadow edges, urban modern fashion, strong visual anchor.", "script": "今天这条视频就带你拆解清楚", "duration": 1.2, "directorRef": "fincher"},
{"id": 9, "shotDesc": "A spider diagram fills the frame — four different trap icons on four sides of the screen all connect to a single glowing core node at the center. Lightning bolts from center to each icon, showing they are secretly linked. Fincher cold blue analytical lighting, precise architectural lines, the core node pulses.", "script": "这四个看似互不关联,底层逻辑却互通的陷阱", "duration": 4.8, "directorRef": "fincher"},
{"id": 10, "shotDesc": "A stylish urban person walks through an open door into white light, the four iron traps behind them dissolve into smoke and shatter. Fincher cold blue backlight silhouette, the door is an analytical white bright light with negative space composition.", "script": "越早警惕,才能越早破解", "duration": 4.8, "directorRef": "fincher"}
]

View File

@@ -34,6 +34,7 @@ node scripts/pipeline.js validate --manifest <path>
| `imageModel` | `gemini` / `mj` | account.json | **init 自动** | | `imageModel` | `gemini` / `mj` | account.json | **init 自动** |
| `videoModel` | `veo3-fast-frames` / `grok-video-3` / `kling` 等 | account.json | **init 自动** | | `videoModel` | `veo3-fast-frames` / `grok-video-3` / `kling` 等 | account.json | **init 自动** |
| `format` | 画幅:`9:16` / `16:9` | account.json | **init 自动** | | `format` | 画幅:`9:16` / `16:9` | account.json | **init 自动** |
| `estimatedVideoDuration` | 视频模型固定时长(秒),顶层冗余字段 | videoModel 查表 | **init 自动**assemble 直接读 |
| `mode` | `single` 单图 / `framePair` 首尾帧 | CLI 参数 | **init 自动** | | `mode` | `single` 单图 / `framePair` 首尾帧 | CLI 参数 | **init 自动** |
| `references` | 参考图数组,从 account.json styles.*.references 搬入 | account.json | **init 自动** | | `references` | 参考图数组,从 account.json styles.*.references 搬入 | account.json | **init 自动** |
| `items` | 素材数组AI 提供创意内容) | CLI --items | **AI → init** | | `items` | 素材数组AI 提供创意内容) | CLI --items | **AI → init** |
@@ -58,18 +59,29 @@ node scripts/pipeline.js validate --manifest <path>
|------|------| |------|------|
| `status` | 固定写 `"pending"` | | `status` | 固定写 `"pending"` |
| `shotDesc` | 英文分镜描述含隐性动势40-80词 | | `shotDesc` | 英文分镜描述含隐性动势40-80词 |
| `script` | **该段的完整原文**不提炼,保留论证、例子、细节| | `script` | **该 shot 的语义子句原文**完整句拆分后的子段,一字不差|
| `duration` | 计划视频时长(秒),来自分镜阶段 | | `duration` | **TTS 估算秒数(= script字数÷5**,必须 ≤ 6s |
| `estimatedAudioDuration` | 同 duration备选别名 |
| `estimatedVideoDuration` | 视频模型固定时长Kling=6s, VEO=8s, Grok=6spipeline init 时自动填入 |
| `imagePrompt` | 英文画面描述(给 Gemini/MJStep 2-A 生成 | | `imagePrompt` | 英文画面描述(给 Gemini/MJStep 2-A 生成 |
| `directorRef` | 导演构图参考tarantino / kitano / fincher三层透传 | | `directorRef` | 导演构图参考tarantino / kitano / fincher三层透传 |
| `keyword` | 关键字氛围词2-6 字assemble 时以花字效果叠加在画面中央。可选 | | `keyword` | 关键字氛围词2-6 字assemble 时以花字效果叠加在画面中央。可选 |
| `confirmed` | 人工确认状态,默认 `false` | | `confirmed` | 人工确认状态,默认 `false` |
**强制约束:**
- **每个 shot 的 `duration`TTS估算必须 ≤ 6s**,否则 pipeline 拒绝执行
- `script` 必须是语义子句,**完整句直接填入多个 shot 是严重错误**
- `estimatedVideoDuration` 在 manifest 初始化时由 `pipeline.js init` 从 videoModel 自动推算:
- `kling``6`
- `veo3-fast` / `veo3-fast-frames``8`
- `grok-video-3``6`
- assemble 阶段通过 `ratio = estimatedVideoDuration / realAudioDuration` 选择适配策略
### Agent 后续回写Step 3-A 视频提示词) ### Agent 后续回写Step 3-A 视频提示词)
| 字段 | 说明 | 写入时机 | | 字段 | 说明 | 写入时机 |
|------|------|---------| |------|------|---------|
| `videoPrompt` | 英文运动描述(给 Grok/VEO描述镜头运动而非内容 | Step 3-A 由 Agent 回写 | | `videoPrompt` | 英文运动描述(给 Grok/VEO/Kling),描述镜头运动而非内容 | Step 3-A 由 Agent 回写 |
### Pipeline 回写(执行后) ### Pipeline 回写(执行后)
@@ -81,10 +93,10 @@ node scripts/pipeline.js validate --manifest <path>
| `url` | 图片 OSS 公网 URL | upload | | `url` | 图片 OSS 公网 URL | upload |
| `confirmed` | 人工确认后设为 `true` | confirm | | `confirmed` | 人工确认后设为 `true` | confirm |
| `video` | 生成的视频路径 | videos | | `video` | 生成的视频路径 | videos |
| `videoDuration` | 视频时长(秒),Grok=6, VEO=8 | videos | | `videoDuration` | 视频实测时长(秒),Kling=6, VEO=8, Grok=6 | videos |
| `videoUrl` | 视频 OSS 公网 URL | videos | | `videoUrl` | 视频 OSS 公网 URL | videos |
| `audio` | TTS 音频路径(多句时为合并后的完整音频) | tts | | `audio` | TTS 音频路径 | tts |
| `audioDuration` | 音频时长(秒) | tts | | `audioDuration` | 音频实测时长(秒) | tts |
| `segments` | 分句音频数组(仅多句时存在),见下方 | tts | | `segments` | 分句音频数组(仅多句时存在),见下方 | tts |
### Agent 审查时可操作 ### Agent 审查时可操作
@@ -220,20 +232,42 @@ TTS 阶段统一生成,单句时数组仅 1 个元素,多句时 N 个元素
## 成片时间线规则 ## 成片时间线规则
> **核心原则**
> - 文案是时间轴唯一锚点
> - TTS 语速固定 1.15x(写死在 qwen-tts.js音频导入 CapCut 时不可调速
> - **音频时长是主时间线**:每个 shot 的 TTS 估算必须 ≤ 视频模型固定时长
> - **视频必须 ≥ 音频**audioDur > videoDur 的 shot 在分镜阶段必须拆分,不允许慢放/冻结
### 时间线估算规则
| 字段 | 计算方式 | 来源 |
|------|---------|------|
| TTS 语速 | **固定 1.15x** | qwen-tts.js 参数 `rate: 1.15`,不可修改 |
| 单 shot TTS 估算 | `script.length ÷ 5`(字/秒) | AI 写入 duration 字段 |
| 视频模型固定时长 | Kling=6s, VEO=8s, Grok=6s | `pipeline.js init` 从 videoModel 推算 |
| ratio | `estimatedVideoDuration / estimatedAudioDuration` | 估算值,供分镜阶段检查 |
| ratio实测 | `videoDuration / audioDuration` | assemble 阶段真实值 |
### 图片模式images ### 图片模式images
图片没有独立时长。TTS 音频时长 = 画面时长。无 TTS 音频的 item 时长为 0跳过不显示 图片没有独立时长。TTS 音频时长 = 画面时长。无 TTS 音频的 item 时长为 0跳过不显示
### 视频模式videos ### 视频模式videos
TTS 音频为主轴,视频通过以下策略适配音频时长: **铁律:视频片段必须 ≥ 音频片段。**
| ratio = videoDur/audioDur | 策略 | 说明 | TTS 音频为主轴,视频通过以下策略适配音频实测时长:
|---------------------------|------|------|
| ratio = estimatedVideoDuration / estimatedAudioDuration | 策略 | 说明 |
|---------------------------------------------------|------|------|
| 0.9 ~ 1.1 | none | 接近匹配,无需调整 | | 0.9 ~ 1.1 | none | 接近匹配,无需调整 |
| > 1.1, ≤ 2 | speed_up | 加速setpts 压缩时间) | | > 1.1, ≤ 2 | **speed_up**(最优) | 视频加速追上音频,音频速率不变 |
| > 2 | trim | 裁剪(截断音频时长 | | > 2 | **trim**(次选) | 视频截断音频时长,损失尾部 |
| < 0.9, ≥ 0.5 | slow_down | 放缓setpts 拉长时间) | | < 0.9 | **禁止 / 打回分镜** | audioDur > videoDur 的 shot 在分镜阶段必须拆分,不允许慢放/冻结 |
| < 0.5 | freeze | 画面停顿(视频原速 + 最后一帧冻结补时长) |
**禁止的策略(已删除):**
- `slow_down`:音频时长超过视频时不允许慢放
- `freeze`:不允许冻结帧补齐
- 音频调速CapCut 导入音频时无 speed 字段1.15x 速率固定
所有策略失败后兜底:截断到目标时长。 所有策略失败后兜底:截断到目标时长。

View File

@@ -0,0 +1,92 @@
# 图文成片工作流问题记录
## 问题一manifest 初始化缺少 file 字段
**现象:**
```
[assemble] 成片失败: The "path" argument must be of type string. Received undefined
```
**根因:**
`pipeline.js init` 生成的 manifest.json 中,每个 item 只有 `shotDesc``script``duration` 等字段,**缺少 `file` 字段**。
`capcut_assemble.js` 依赖 `item.file` 来定位本地图片:
```js
const filePath = path.join(inputDir, item.file)
return fs.existsSync(filePath)
```
没有 `file` 字段时,`path.join(inputDir, undefined)``undefined` → 报错。
**修复:**
手动给每个 item 补上 `file` 字段:
```js
m.items.forEach((item, i) => {
item.file = 'images/scene_' + String(i+1).padStart(2,'0') + '_' + slug + '.jpeg'
})
```
**建议改进:**
`pipeline.js init` 应自动根据 items 索引和 slug 生成 `file` 字段,与后续 assemble 阶段无缝衔接。
---
## 问题二:并行生图命令的 cwd 问题
**现象:**
```bash
# 6 个并行命令,其中 5 个报错
cd .claude/skills/video-from-script/scripts && node gemini-image-generator.js ...
# Exit code 1: no such file or directory
```
**根因:**
5 个并行命令在解析时zsh 把 `.claude/...` 路径中 `.` 视为当前目录的相对路径,而并行任务可能在不同 cwd 下执行,导致路径解析失败。
**修复:**
改用绝对路径:
```bash
SCRIPTS="/Users/lc/Desktop/CLAUDE/video-create/.claude/skills/video-from-script/scripts"
node "$SCRIPTS/gemini-image-generator.js" generate "..." -o "$OUT" -r 9:16
```
**建议改进:**
CLI 命令始终使用绝对路径,避免相对路径在并行环境下的歧义。
---
## 问题三shot 6 在 mv 重命名时被遗漏
**现象:**
6 张图生完后,重命名时只有 5 张被正确命名为 `scene_0X_xxx.jpeg`,缺少 `scene_06_跪着.jpeg`
**根因:**
mv 命令基于修改时间排序zsh glob `generated_*.jpeg` 只匹配当时存在的文件。6张图的生成时间戳不同重命名脚本从最旧的文件开始取tail -1但脚本顺序与实际时间顺序可能不匹配。
**修复:**
直接对生成的临时文件重命名为固定名称,不依赖时间排序逻辑。
**建议改进:**
pipeline.js 的生图阶段应直接输出为 `scene_{NN}_{slug}.jpeg`,而非先生成 `generated_*.jpeg` 再重命名。
---
## 问题四pipeline.js assemble 阶段的路径解析 Bug
**现象:**
`pipeline.js run --phase tts,assemble` 时 tts 正常,但 assemble 阶段找不到文件而报错。直接调用 `capcut_assemble.js --input <dir>` 则正常。
**根因:**
pipeline.js 在调用 assemble 时,传递的 input 路径为相对路径,且未正确设置 `item.file` 字段,导致 assemble 内部 `path.join(inputDir, item.file)` 得到 undefined。
**建议改进:**
`pipeline.js run --phase assemble` 前应先检查 items 是否都有 `file` 字段,缺失时自动补充。
---
## 建议的改进方向
1. **`pipeline.js init`** 自动生成 `file` 字段,与图片命名规范一致
2. **CLI 命令** 统一使用绝对路径,避免 cwd 歧义
3. **生图脚本** 直接输出为 `scene_XX_xxx.jpeg`,消除重命名步骤
4. **`pipeline.js validate`** 增加 assemble 阶段的前置检查items.file + items.audio 完整性)

View File

@@ -22,7 +22,7 @@ const { buildTimeline, adjustVideoSpeed } = require('./lib/capcut-timeline')
const { const {
loadAccountConfig, loadSubtitleStyle, loadAccountConfig, loadSubtitleStyle,
loadKenBurns, loadTransitions, loadKenBurns, loadTransitions,
addImages, addVideos, addKenBurns, addImages, addVideos, addSlotsLocally,
addVoiceover, addBGM, addVoiceover, addBGM,
addSubtitles, addSubtitles,
addEffects, addFilter, addEffects, addFilter,
@@ -65,23 +65,43 @@ async function batchUploadToOSS(inputDir, files, concurrency = 3) {
async function batchUploadAudio(inputDir, items) { async function batchUploadAudio(inputDir, items) {
const urls = {} const urls = {}
for (const item of items) { for (const item of items) {
if (!item.audio || item.audio.startsWith('http')) { // 处理主音频
if (item.audio) urls[item.audio] = item.audio if (item.audio && !item.audio.startsWith('http')) {
continue if (!urls[item.audio]) {
const filePath = path.isAbsolute(item.audio)
? item.audio
: path.resolve(inputDir, item.audio)
if (fs.existsSync(filePath)) {
try {
urls[item.audio] = await uploadToOSS(filePath)
console.log(` 上传: ${path.basename(filePath)} -> OK`)
} catch (err) {
console.error(` 上传失败: ${path.basename(filePath)} - ${err.message}`)
}
}
}
} else if (item.audio) {
urls[item.audio] = item.audio
} }
if (urls[item.audio]) continue // 处理分段音频
const filePath = path.isAbsolute(item.audio) if (item.segments && item.segments.length > 0) {
? item.audio for (const seg of item.segments) {
: path.resolve(inputDir, item.audio) if (!seg.audio || seg.error) continue
if (!fs.existsSync(filePath)) { if (urls[seg.audio]) continue
console.error(` 音频文件不存在: ${filePath}`) const filePath = path.isAbsolute(seg.audio)
continue ? seg.audio
} : path.resolve(inputDir, seg.audio)
try { if (!fs.existsSync(filePath)) {
urls[item.audio] = await uploadToOSS(filePath) console.error(` 音频文件不存在: ${filePath}`)
console.log(` 上传: ${path.basename(filePath)} -> OK`) continue
} catch (err) { }
console.error(` 上传失败: ${path.basename(filePath)} - ${err.message}`) try {
urls[seg.audio] = await uploadToOSS(filePath)
console.log(` 上传: ${path.basename(filePath)} -> OK`)
} catch (err) {
console.error(` 上传失败: ${path.basename(filePath)} - ${err.message}`)
}
}
} }
} }
return urls return urls
@@ -288,7 +308,11 @@ async function assemble(args) {
} }
} }
} }
await addVideos(draftUrl, inputDir, items, timeline, width, height, transitionConfig) const segmentIds = await addVideos(draftUrl, inputDir, items, timeline, width, height, transitionConfig)
// 将 segment_ids 附加到 items供后续 addSlotsLocally 使用
if (segmentIds && segmentIds.length > 0) {
items.forEach((item, i) => { item._segmentId = segmentIds[i] || null })
}
} }
// -- Ken Burns -- // -- Ken Burns --
@@ -383,6 +407,13 @@ async function assemble(args) {
await syncToLocalJianying(draftUrl, draftId, totalDurationUs) await syncToLocalJianying(draftUrl, draftId, totalDurationUs)
console.log(' 同步完成\n') console.log(' 同步完成\n')
// -- 视频轨道 slot 写入(在 syncToLocalJianying 之后执行,此时本地草稿文件已存在)--
if (mode !== 'images') {
step++; console.log(`[${step}/${totalSteps}] 写入视频轨道时间线...`)
await addSlotsLocally(draftUrl, items, timeline, null, { draftId })
console.log(' 视频轨道写入完成\n')
}
// -- 云渲染(可选)-- // -- 云渲染(可选)--
if (apiKey) { if (apiKey) {
console.log('提交云渲染...') console.log('提交云渲染...')

View File

@@ -3,12 +3,15 @@
* *
* 核心算法模块。纯函数 + ffmpeg自包含可测试。 * 核心算法模块。纯函数 + ffmpeg自包含可测试。
* *
* 规则: * 铁律(固化,不可绕过):
* 音频生成后不可调速TTS=1.15xCapCut无speed字段
* 视频:始终配合音频时长(只允许加速/截断,不允许慢放/冻结)
*
* 时间线规则:
* 图片模式: TTS 音频时长 = 画面时长,无音频 = 跳过 * 图片模式: TTS 音频时长 = 画面时长,无音频 = 跳过
* 视频模式: TTS 为主轴,视频通过策略适配 * 视频模式: TTS 为主轴,视频通过策略适配
* 视频比音频长 → 加速(≤2x) / 裁剪(>2x) * 视频比音频长 → 加速(≤2x) / 裁剪(>2x)
* 视频比音频短 → 放缓(≥0.5x) / 画面停顿(<0.5x) * 视频比音频短 → 禁止!应在分镜阶段拆分 shot不允许慢放/冻结补齐
* 所有策略失败 → 兜底截断
*/ */
const fs = require('fs') const fs = require('fs')
@@ -20,6 +23,20 @@ const { US } = require('./capcut-api')
// 时间线构建 // 时间线构建
// ============================================================================ // ============================================================================
/**
* 构建时间线条目
*
* @param {Array} items - manifest items
* @returns {Array} timeline entries
*
* 策略选择(固化,按 ratio = videoDur / audioDur
* ≥ 1.1, ≤ 2 → speed_up (视频加速追上音频,最优)
* > 2 → trim (视频截断至音频时长)
* 0.9 ~ 1.1 → none (接近匹配,无需调整)
* < 0.9 → 禁止!音频时长超过视频,分镜阶段未正确拆分 shot
*
* 铁律:不允许 slow_down / freeze不允许音频调速
*/
function buildTimeline(items) { function buildTimeline(items) {
let offset = 0 let offset = 0
return items.map(item => { return items.map(item => {
@@ -46,7 +63,7 @@ function buildTimeline(items) {
return entry return entry
} }
// 视频模式:策略选择 // 视频模式:策略选择(铁律:不允许音频>视频)
const ratio = videoDur / audioDur const ratio = videoDur / audioDur
if (ratio > 1.1) { if (ratio > 1.1) {
@@ -59,23 +76,25 @@ function buildTimeline(items) {
offset += dur offset += dur
return entry return entry
} }
} else if (ratio < 0.9) { } else if (ratio >= 0.9) {
if (ratio >= 0.5) { // 0.9 ~ 1.1:无需调整
const entry = { start: offset, end: offset + dur, duration: dur, speed: ratio, strategy: 'slow_down' }
offset += dur
return entry
} else {
const entry = {
start: offset, end: offset + dur, duration: dur, speed: 1,
strategy: 'freeze', freezeExtra: dur - videoDur,
}
offset += dur
return entry
}
} else {
const entry = { start: offset, end: offset + dur, duration: dur, speed: 1, strategy: 'none' } const entry = { start: offset, end: offset + dur, duration: dur, speed: 1, strategy: 'none' }
offset += dur offset += dur
return entry return entry
} else {
// ratio < 0.9:音频时长超过视频!
// 铁律禁止:不允许慢放/冻结/拼接补齐。此情况应在分镜阶段拆分 shot。
// 强制截断并打印错误标记,由主 Agent 上报给用户/打回分镜重做。
const entry = {
start: offset, end: offset + dur, duration: dur, speed: 1,
strategy: 'FORBIDDEN_audio_gt_video',
ratio: parseFloat(ratio.toFixed(3)),
videoDur: parseFloat((videoDur / US).toFixed(2)),
audioDur: parseFloat((audioDur / US).toFixed(2)),
error: '音频时长(' + (audioDur / US).toFixed(2) + 's) > 视频时长(' + (videoDur / US).toFixed(2) + 's),分镜阶段 shot 未正确拆分,请打回重新切割',
}
offset += dur
return entry
} }
}) })
} }
@@ -87,16 +106,18 @@ function buildTimeline(items) {
/** /**
* ffmpeg 视频调整:根据策略适配音频时长 * ffmpeg 视频调整:根据策略适配音频时长
* *
* 策略(按 ratio = videoDur / audioDur 选择): * 允许策略(按 ratio = videoDur / audioDur 选择):
* speed_up (ratio > 1.1, ≤2x) → setpts 压缩时间(加速) * speed_up (ratio > 1.1, ≤2x) → setpts 压缩时间(加速),最优
* trim (ratio > 2x) → 截断到目标时长 * trim (ratio > 2x) → 截断到目标时长,次选
* slow_down (ratio < 0.9, ≥0.5x) → setpts 拉长时间(慢放)
* freeze (ratio < 0.5x) → 视频原速 + 最后一帧冻结补时长
* none (0.9~1.1) → 无需调整 * none (0.9~1.1) → 无需调整
* *
* 禁止策略(已删除):
* slow_down (ratio < 0.9) → ❌ 音频不可调速!
* freeze (ratio < 0.5) → ❌ 不允许冻结帧补齐!
*
* 所有策略失败后兜底:截断到目标时长 * 所有策略失败后兜底:截断到目标时长
*/ */
async function adjustVideoSpeed(videoPath, targetDurationSec, strategy = 'none', speed = 1, freezeExtraUs = 0) { async function adjustVideoSpeed(videoPath, targetDurationSec, strategy = 'none', speed = 1) {
if (!fs.existsSync(videoPath)) return videoPath if (!fs.existsSync(videoPath)) return videoPath
if (strategy === 'none') return videoPath if (strategy === 'none') return videoPath
@@ -150,72 +171,9 @@ async function adjustVideoSpeed(videoPath, targetDurationSec, strategy = 'none',
console.log(` 加速: ${videoDur.toFixed(1)}s → ${targetDurationSec.toFixed(1)}s (${speedVal}x)`) console.log(` 加速: ${videoDur.toFixed(1)}s → ${targetDurationSec.toFixed(1)}s (${speedVal}x)`)
resolve(outPath) resolve(outPath)
}) })
} else if (strategy === 'slow_down') {
const factor = (1 / speed).toFixed(3)
execFile('ffmpeg', [
'-y', '-i', videoPath,
'-filter_complex', `setpts=PTS*${factor}`,
'-an',
outPath
], { timeout: 30000 }, (err) => {
if (err) {
console.log(` 放缓失败,兜底截断: ${err.message}`)
fallbackTrim(resolve)
return
}
console.log(` 放缓: ${videoDur.toFixed(1)}s → ${targetDurationSec.toFixed(1)}s (${speed.toFixed(2)}x speed)`)
resolve(outPath)
})
} else if (strategy === 'freeze') {
const freezeSec = freezeExtraUs / US
execFile('ffmpeg', [
'-y', '-i', videoPath,
'-filter_complex', `tpad=stop=-1:stop_duration=${freezeSec.toFixed(3)}`,
'-an',
outPath
], { timeout: 30000 }, (err) => {
if (err) {
console.log(` tpad freeze 失败,尝试 concat 方案: ${err.message}`)
const lastFrame = videoPath.replace(/(\.\w+)$/, '_lastframe.png')
const frozenVideo = videoPath.replace(/(\.\w+)$/, '_frozen.mp4')
execFile('ffmpeg', [
'-y', '-sseof', '-0.1', '-i', videoPath,
'-frames:v', '1', lastFrame
], { timeout: 10000 }, (err2) => {
if (err2) { console.log(` concat 方案也失败,兜底截断`); fallbackTrim(resolve); return }
execFile('ffmpeg', [
'-y', '-loop', '1', '-i', lastFrame,
'-t', String(freezeSec.toFixed(3)),
'-pix_fmt', 'yuv420p',
'-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
frozenVideo
], { timeout: 15000 }, (err3) => {
if (err3) {
try { fs.unlinkSync(lastFrame) } catch (_) {}
console.log(` 冻结帧视频生成失败,兜底截断`)
fallbackTrim(resolve)
return
}
const concatList = path.join(path.dirname(videoPath), '_freeze_concat.txt')
fs.writeFileSync(concatList, `file '${videoPath}'\nfile '${frozenVideo}'\n`)
execFile('ffmpeg', [
'-y', '-f', 'concat', '-safe', '0', '-i', concatList,
'-c', 'copy', outPath
], { timeout: 30000 }, (err4) => {
try { fs.unlinkSync(lastFrame); fs.unlinkSync(frozenVideo); fs.unlinkSync(concatList) } catch (_) {}
if (err4) { console.log(` 拼接失败,兜底截断`); fallbackTrim(resolve); return }
console.log(` 画面停顿: ${videoDur.toFixed(1)}s + 冻结 ${freezeSec.toFixed(1)}s = ${targetDurationSec.toFixed(1)}s`)
resolve(outPath)
})
})
})
return
}
console.log(` 画面停顿: ${videoDur.toFixed(1)}s + 冻结 ${freezeSec.toFixed(1)}s = ${targetDurationSec.toFixed(1)}s`)
resolve(outPath)
})
} else { } else {
resolve(videoPath) // 未知策略,兜底截断
fallbackTrim(resolve)
} }
}) })
}) })

View File

@@ -3,6 +3,10 @@
* *
* 所有 add* 函数 + 转场策略 + 账号配置读取。 * 所有 add* 函数 + 转场策略 + 账号配置读取。
* Agent 修改字幕风格、Ken Burns、转场、特效等只需关注此文件。 * Agent 修改字幕风格、Ken Burns、转场、特效等只需关注此文件。
*
* 音频策略(固化铁律):
* - 音频由 TTS 1.15x 生成,导入 CapCut 时无 speed 字段(不可调速)
* - 每个 item 的 segments[] 逐段添加,各段 start 按 startOffset 精确对齐
*/ */
const path = require('path') const path = require('path')
@@ -303,33 +307,233 @@ async function addVideos(draftUrl, inputDir, items, timeline, width, height, tra
return allSegmentIds return allSegmentIds
} }
// ============================================================================
// 将 segment 写入视频轨道时间线slot
// 背景add_videos 只负责把视频加入素材库,不自动上时间线。
// 此函数在 add_videos 成功后调用,将每个 segment_id 写入第一个 video track。
// ============================================================================
async function addSlots(draftUrl, items, timeline) {
const { api: capcutApi, US } = require('./capcut-api')
const { getManifestDir } = require('./pipeline-utils')
const path = require('path')
// 获取当前云端草稿的 draft_content获取第一个 video track 的 id
let draftData
try {
draftData = (await capcutApi('get_draft', { draft_url: draftUrl })).data || {}
} catch (err) {
// get_draft 接口不可用,尝试从本地 manifest 目录寻找草稿
const manifestDir = path.dirname(draftUrl.startsWith('http') ? draftUrl : '')
console.log(' get_draft 不可用,切换本地写入模式')
return addSlotsLocally(draftUrl, items, timeline)
}
const tracks = draftData.tracks || []
const videoTrack = tracks.find(t => t.type === 'video')
if (!videoTrack) {
console.log(' 未找到 video track跳过 slot 写入')
return
}
// 构造 slot 数据
const slots = []
for (let i = 0; i < items.length; i++) {
const item = items[i]
const tl = timeline[i]
const segId = item.segmentId || item._segmentId
if (!segId) continue
const slotId = generateUUID()
slots.push({
id: slotId,
material_id: segId,
track_id: videoTrack.id,
render_index: i,
type: 'video',
common_property: {
start_time: tl.start,
source_timerange: { start: 0, duration: tl.duration },
target_timerange: { start: tl.start, duration: tl.duration },
is_avatar: false,
audio_fade: { fade_in_duration: 0, fade_out_duration: 0 },
volume: 1.0,
},
})
}
if (slots.length === 0) {
console.log(' 无有效 slot 数据,跳过')
return
}
// 通过 add_slots API 写入
try {
await capcutApi('add_slots', {
draft_url: draftUrl,
slots: JSON.stringify(slots),
})
console.log(` 已写入 ${slots.length} 个 slot 到视频轨道`)
} catch (err) {
// API 不支持时,降级为本地写入
console.log(` add_slots API 不可用: ${err.message},降级为本地写入`)
await addSlotsLocally(draftUrl, items, timeline, videoTrack.id)
}
}
// 直接写入本地 draft_content.json 的 slot
// options.draftId: 可选,直接指定 draftId优先使用否则从 draftUrl 提取
async function addSlotsLocally(draftUrl, items, timeline, trackId, options = {}) {
const { api: capcutApi, US } = require('./capcut-api')
const fs = require('fs')
// 优先使用 options.draftId否则从 draftUrl 提取
let draftId = options.draftId || null
if (!draftId) {
try {
draftId = new URL(draftUrl).searchParams.get('draft_id')
} catch {
console.log(' 无法解析 draftUrl跳过本地 slot 写入')
return
}
}
if (!draftId) {
console.log(' 无法提取 draft_id跳过本地 slot 写入')
return
}
const { getConfig } = require('./capcut-api')
const jianyingPath = getConfig().jianyingDraftPath
const draftPath = path.join(jianyingPath, draftId, 'draft_content.json')
if (!fs.existsSync(draftPath)) {
console.log(` 本地草稿不存在: ${draftPath},跳过 slot 写入`)
return
}
let draft
try {
draft = JSON.parse(fs.readFileSync(draftPath, 'utf-8'))
} catch {
console.log(' draft_content.json 读取失败,跳过')
return
}
// 找到第一个 video track
const videoTrack = trackId
? draft.tracks.find(t => t.id === trackId)
: draft.tracks.find(t => t.type === 'video')
if (!videoTrack) {
console.log(' 未找到 video track跳过')
return
}
const slots = []
for (let i = 0; i < items.length; i++) {
const item = items[i]
const tl = timeline[i]
const segId = item.segmentId || item._segmentId
if (!segId) {
// 尝试从 materials.videos 匹配
const fname = item.video ? path.basename(item.video) : ''
const matVideo = (draft.materials.videos || []).find(v => {
const matFname = path.basename(v.path || '')
return fname && matFname.includes(fname.replace('videos/', ''))
})
if (matVideo) {
items[i]._segmentId = matVideo.id
slots.push(buildSlot(matVideo.id, videoTrack.id, i, tl, US))
}
} else {
slots.push(buildSlot(segId, videoTrack.id, i, tl, US))
}
}
if (slots.length > 0) {
videoTrack.slots = slots
draft.duration = timeline.length > 0 ? timeline[timeline.length - 1].end : 0
fs.writeFileSync(draftPath, JSON.stringify(draft, null, 2), 'utf-8')
console.log(` 已本地写入 ${slots.length} 个 slot 到视频轨道`)
// 触发剪映扫描
triggerDirScan(path.dirname(draftPath))
}
}
function buildSlot(segId, trackId, index, tl, US) {
return {
id: generateUUID(),
material_id: segId,
track_id: trackId,
render_index: index,
type: 'video',
common_property: {
start_time: tl.start,
source_timerange: { start: 0, duration: tl.duration },
target_timerange: { start: tl.start, duration: tl.duration },
is_avatar: false,
audio_fade: { fade_in_duration: 0, fade_out_duration: 0 },
volume: 1.0,
},
}
}
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16).toUpperCase()
})
}
function triggerDirScan(dir) {
const { execFile } = require('child_process')
const tmp = dir + '.slot_tmp'
if (process.platform === 'darwin') {
execFile('rsync', ['-a', dir + '/', tmp], (err) => {
try { require('fs').rmSync(tmp, { recursive: true, force: true }) } catch {}
})
}
}
// ============================================================================ // ============================================================================
// 添加 TTS 配音 // 添加 TTS 配音
// ============================================================================ // ============================================================================
async function addVoiceover(draftUrl, inputDir, items, timeline, audioUrls = {}) { async function addVoiceover(draftUrl, inputDir, items, timeline, audioUrls = {}) {
const audioItems = items.filter(item => item.audio) // 优先使用 segments[] 逐段添加(精确对齐)
if (audioItems.length === 0) { // 无 segments 时降级为旧的整段方式
console.log(' 无 TTS 音频文件,跳过') const segmentsFlat = []
return
}
const audioInfos = []
const resolveAudio = (relPath) => {
if (relPath.startsWith('http')) return relPath
if (audioUrls[relPath]) return audioUrls[relPath]
return path.isAbsolute(relPath) ? relPath : path.resolve(inputDir, relPath)
}
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[i] const item = items[i]
const tl = timeline[i] const tl = timeline[i]
if (!item.audio) continue
if (item.audio) { if (item.segments && item.segments.length > 0) {
const audioUrl = resolveAudio(item.audio) // 使用 segments 精确添加
for (const seg of item.segments) {
if (!seg.audio || seg.error) continue
const audioUrl = seg.audio.startsWith('http')
? seg.audio
: (audioUrls[seg.audio] || path.resolve(inputDir, seg.audio))
const segDurUs = Math.round(seg.duration * US)
const segStartUs = tl.start + Math.round(seg.startOffset * US)
segmentsFlat.push({
audio_url: audioUrl,
start: segStartUs,
end: segStartUs + segDurUs,
duration: segDurUs,
volume: 1.0,
})
}
} else {
// 降级:整段添加
const audioUrl = item.audio.startsWith('http')
? item.audio
: (audioUrls[item.audio] || path.resolve(inputDir, item.audio))
const audioDurUs = item.audioDuration ? item.audioDuration * US : tl.duration const audioDurUs = item.audioDuration ? item.audioDuration * US : tl.duration
segmentsFlat.push({
audioInfos.push({
audio_url: audioUrl, audio_url: audioUrl,
start: tl.start, start: tl.start,
end: tl.start + audioDurUs, end: tl.start + audioDurUs,
@@ -339,17 +543,26 @@ async function addVoiceover(draftUrl, inputDir, items, timeline, audioUrls = {})
} }
} }
if (audioInfos.length === 0) { if (segmentsFlat.length === 0) {
console.log(' 无可用音频,跳过配音') console.log(' 无 TTS 音频文件,跳过')
return return
} }
await api('add_audios', { // 逐个添加音频CapCut API 批量添加不稳定)
draft_url: draftUrl, let addedCount = 0
audio_infos: JSON.stringify(audioInfos), for (const audioInfo of segmentsFlat) {
}) try {
const ossCount = audioInfos.filter(a => a.audio_url.startsWith('http')).length await api('add_audios', {
console.log(` 已添加 ${audioInfos.length} 段 TTS 配音 (${ossCount > 0 ? `${ossCount} 段 OSS + ` : ''}${audioInfos.length - ossCount} 段本地)`) draft_url: draftUrl,
audio_infos: JSON.stringify([audioInfo]),
})
addedCount++
} catch (err) {
console.error(` 音频添加失败: ${err.message.slice(0, 80)}`)
}
}
const ossCount = segmentsFlat.filter(a => a.audio_url.startsWith('http')).length
console.log(` 已添加 ${addedCount}/${segmentsFlat.length} 段 TTS 配音 (${ossCount} 段 OSS)`)
} }
// ============================================================================ // ============================================================================
@@ -402,7 +615,24 @@ async function addSubtitles(draftUrl, items, timeline, style = {}, split = false
const tl = timeline[i] const tl = timeline[i]
if (split) { if (split && item.segments && item.segments.length > 0) {
// 精确字幕模式:使用 segments 实测时长,逐段添加字幕
for (const seg of item.segments) {
if (seg.error || !seg.text) continue
const segStartUs = tl.start + Math.round(seg.startOffset * US)
const segDurUs = Math.round(seg.duration * US)
const cap = {
start: segStartUs,
end: segStartUs + segDurUs,
text: seg.text,
}
applyAnimationProps(cap, animStyle)
captions.push(cap)
}
} else if (split) {
// 降级:按字符比例分配(无 segments 时)
const sentences = splitTextIntoSentences(text) const sentences = splitTextIntoSentences(text)
if (sentences.length === 0) continue if (sentences.length === 0) continue
@@ -447,9 +677,7 @@ async function addSubtitles(draftUrl, items, timeline, style = {}, split = false
return return
} }
await api('add_captions', { const commonStyle = {
draft_url: draftUrl,
captions: JSON.stringify(captions),
font: style.font || null, font: style.font || null,
font_size: style.fontSize || 15, font_size: style.fontSize || 15,
text_color: style.color || '#ffffff', text_color: style.color || '#ffffff',
@@ -472,9 +700,23 @@ async function addSubtitles(draftUrl, items, timeline, style = {}, split = false
transform_x: 0, transform_x: 0,
transform_y: style.transformY || 0, transform_y: style.transformY || 0,
style_text: 0, style_text: 0,
}) }
console.log(` 已添加 ${captions.length} 条字幕${split ? ' (分句模式)' : ''} (字体: ${style.font || '默认'}, 动画: ${animStyle.inAnimation || '无'}${animStyle.outAnimation || '无'})`) // 逐条添加字幕CapCut API 批量添加不稳定)
let addedCount = 0
for (const cap of captions) {
try {
await api('add_captions', {
draft_url: draftUrl,
captions: JSON.stringify([cap]),
...commonStyle,
})
addedCount++
} catch (err) {
console.error(` 字幕添加失败: ${err.message.slice(0, 80)}`)
}
}
console.log(` 已添加 ${addedCount}/${captions.length} 条字幕${split ? ' (分句模式)' : ''} (字体: ${style.font || '默认'}, 动画: ${animStyle.inAnimation || '无'}${animStyle.outAnimation || '无'})`)
} }
// ============================================================================ // ============================================================================
@@ -583,6 +825,8 @@ module.exports = {
addBGM, addBGM,
addSubtitles, addSubtitles,
addKeywordOverlays, addKeywordOverlays,
addSlots,
addSlotsLocally,
addEffects, addEffects,
addFilter, addFilter,
} }

View File

@@ -72,6 +72,28 @@ function initManifest(options) {
console.log(`${refsWithoutUrl.length} 个参考图缺少 OSS URLimages 阶段会自动上传`) console.log(`${refsWithoutUrl.length} 个参考图缺少 OSS URLimages 阶段会自动上传`)
} }
// 从 videoModel 推算固定时长(秒)
const videoModelFixedDurations = {
'kling': 6,
'kling-v2-5-turbo': 6,
'veo3-fast': 8,
'veo3-fast-frames': 8,
'grok-video-3': 6,
}
const estimatedVideoDuration = videoModelFixedDurations[options.videoModel || accountConfig.videoModel] || 6
// 校验时长约束
for (let i = 0; i < rawItems.length; i++) {
const item = rawItems[i]
const dur = Number(item.duration) || 5
if (dur > estimatedVideoDuration) {
console.error(`错误: items[${i}] 的 TTS 估算 duration=${dur}s > videoModel 固定时长 ${estimatedVideoDuration}s`)
console.error(` 必须先拆分 shot 再执行 init`)
console.error(` script: "${item.script}"`)
process.exit(1)
}
}
// 构建 items // 构建 items
const items = rawItems.map((raw, i) => { const items = rawItems.map((raw, i) => {
const slug = slugify(raw.shotDesc || raw.script || `scene_${i + 1}`) const slug = slugify(raw.shotDesc || raw.script || `scene_${i + 1}`)
@@ -81,7 +103,8 @@ function initManifest(options) {
file: `images/scene_${String(i + 1).padStart(2, '0')}_${slug}.jpeg`, file: `images/scene_${String(i + 1).padStart(2, '0')}_${slug}.jpeg`,
shotDesc: raw.shotDesc || '', shotDesc: raw.shotDesc || '',
script: raw.script || '', script: raw.script || '',
duration: raw.duration || 5, duration: Number(raw.duration) || 5,
estimatedVideoDuration,
imagePrompt: raw.imagePrompt, imagePrompt: raw.imagePrompt,
confirmed: false, confirmed: false,
} }
@@ -102,8 +125,10 @@ function initManifest(options) {
references, references,
...(accountConfig.ttsVoice ? { ttsVoice: accountConfig.ttsVoice } : {}), ...(accountConfig.ttsVoice ? { ttsVoice: accountConfig.ttsVoice } : {}),
...(accountConfig.ttsInstruction ? { ttsInstruction: accountConfig.ttsInstruction } : {}), ...(accountConfig.ttsInstruction ? { ttsInstruction: accountConfig.ttsInstruction } : {}),
...(accountConfig.ttsRate ? { ttsRate: accountConfig.ttsRate } : {}), // 铁律ttsRate 写死 1.15x,不允许配置覆盖(除非显式传入)
ttsRate: options.ttsRate || 1.15,
items, items,
estimatedVideoDuration, // 顶层冗余,便于 assemble 直接读取
} }
// 创建输出目录(自增序号) // 创建输出目录(自增序号)

View File

@@ -1,13 +1,100 @@
/** /**
* Phase: tts — 语音合成(整段合成) * Phase: tts — 语音合成(先分段,后合成)
* *
* 每个 item 的 script 整段合成一个音频文件,保留自然语调 * 核心变化:音频分段优先于生图
* item.audio 指向完整音频item.audioDuration 为总时长。 *
* 字幕切分由组装阶段按字符比例分配,不在 TTS 阶段处理。 * 1. 在生成图片之前,先将文案按语义断点切分为多个音频片段
* 2. 每个片段时长 < videoModel 固定时长Kling=6s
* 3. 逐段合成,记录实测时长,写入 manifest.segments[]
* 4. manifest.items[n].segments = [{text, audio, duration, startOffset}, ...]
* 5. manifest.items[n].audioDuration = 片段总和(供 assemble 计算 ratio
*
* 流程顺序变为tts → images → upload → videos → assemble
*/ */
const path = require('path') const path = require('path')
const { saveManifest, ensureDir, log, getManifestDir } = require('./pipeline-utils') const { saveManifest, ensureDir, log, getManifestDir, splitTextIntoSentences } = require('./pipeline-utils')
/**
* 在语义断点处将文案切分为音频片段
* 每段时长(估算)必须 < videoDuration且尽量接近最佳 ratio 接近1.0
*
* @param {string} text - 完整文案
* @param {number} videoDur - 视频模型固定时长(秒),如 6
* @param {number} charsPerSec - 语速(字/秒),固定 5
* @returns {Array<{text, estimatedDuration}>}
*/
function splitIntoAudioSegments(text, videoDur, charsPerSec = 5) {
// 优先在自然断点切分(句号/感叹号/分号)
const naturalBreaks = splitTextIntoSentences(text)
if (naturalBreaks.length <= 1) {
// 无自然断点:在半段处(含小数点)切分
const chars = text.length
const estimatedTotal = chars / charsPerSec
if (estimatedTotal <= videoDur) {
// 整段可容纳
return [{ text, estimatedDuration: estimatedTotal }]
}
// 无法单段容纳,在中间逗号处切
const mid = Math.floor(chars / 2)
const breakIdx = text.indexOf('', mid)
if (breakIdx > 0) {
return [
{ text: text.slice(0, breakIdx + 1), estimatedDuration: (breakIdx + 1) / charsPerSec },
{ text: text.slice(breakIdx + 1), estimatedDuration: (chars - breakIdx - 1) / charsPerSec },
]
}
// 强制按字数切
const halfChars = Math.floor(chars / 2)
return [
{ text: text.slice(0, halfChars), estimatedDuration: halfChars / charsPerSec },
{ text: text.slice(halfChars), estimatedDuration: (chars - halfChars) / charsPerSec },
]
}
// 多个自然句:逐句判断,合并短句
const result = []
let currentText = ''
let currentEstDur = 0
for (let i = 0; i < naturalBreaks.length; i++) {
const sentence = naturalBreaks[i]
const sentenceLen = sentence.length
const sentenceEstDur = sentenceLen / charsPerSec
if (currentEstDur + sentenceEstDur <= videoDur) {
// 可以合并到当前段
currentText += sentence + '。'
currentEstDur += sentenceEstDur
} else {
// 先保存当前段
if (currentText) {
result.push({ text: currentText.trim(), estimatedDuration: currentEstDur })
}
currentText = sentence + '。'
currentEstDur = sentenceEstDur
// 单句本身超长(超 videoDur
if (sentenceEstDur > videoDur) {
// 按半段切
const halfLen = Math.floor(sentenceLen / 2)
const half1 = sentence.slice(0, halfLen)
const half2 = sentence.slice(halfLen)
// 回退上一段,用两个半段替代
result.pop()
result.push({ text: half1, estimatedDuration: halfLen / charsPerSec })
currentText = half2 + '。'
currentEstDur = (sentenceLen - halfLen) / charsPerSec
}
}
}
if (currentText) {
result.push({ text: currentText.trim(), estimatedDuration: currentEstDur })
}
return result
}
async function phaseTts(manifest, manifestPath, options = {}) { async function phaseTts(manifest, manifestPath, options = {}) {
const dir = getManifestDir(manifestPath) const dir = getManifestDir(manifestPath)
@@ -16,38 +103,89 @@ async function phaseTts(manifest, manifestPath, options = {}) {
const { synthesize } = require('../qwen-tts') const { synthesize } = require('../qwen-tts')
const items = manifest.items.filter(it => const videoDur = manifest.estimatedVideoDuration || 6
it.status === 'done' && (it.script || it.text) && !it.audio const ttsRate = manifest.ttsRate || 1.15
)
if (items.length === 0) { log('tts', '无待处理 item跳过'); return }
log('tts', `${items.length}`) const items = manifest.items.filter(it =>
(it.script || it.text) && !it.audio
)
if (items.length === 0) { log('tts', '无待处理 item已合成跳过'); return }
log('tts', `${items.length} 段, 视频固定时长=${videoDur}s, TTS语速=${ttsRate}x`)
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[i] const item = items[i]
const idx = i + 1 const idx = i + 1
const fullText = item.script || item.text const fullText = (item.script || item.text).trim()
try { // Step 1: 计算音频分段
const { filePath, duration } = await synthesize(fullText, { const rawSegments = splitIntoAudioSegments(fullText, videoDur)
outputDir: audioDir, log('tts', `[${idx}/${items.length}] 原始分段: ${rawSegments.length}`)
id: String(item.id || idx), for (const seg of rawSegments) {
voice: manifest.ttsVoice || undefined, log('tts', ` 分段估算: ${seg.estimatedDuration.toFixed(2)}s / ${seg.text.slice(0, 20)}...`)
instruction: manifest.ttsInstruction || undefined,
rate: manifest.ttsRate || undefined,
})
const totalDuration = Math.round(duration * 1000) / 1000
item.audio = path.relative(dir, filePath).replace(/\\/g, '/')
item.audioDuration = totalDuration
log('tts', `[${idx}/${items.length}] ${totalDuration.toFixed(1)}s: ${fullText.substring(0, 30)}...`)
} catch (err) {
item.status = 'failed'
item.error = `TTS失败: ${err.message}`
log('tts', `[${idx}/${items.length}] 失败: ${err.message}`)
} }
// Step 2: 逐段合成
const segments = []
let globalOffset = 0
for (let j = 0; j < rawSegments.length; j++) {
const segInput = rawSegments[j]
const segId = `${item.id}_${j + 1}`
try {
const { filePath, duration: realDuration } = await synthesize(segInput.text, {
outputDir: audioDir,
id: segId,
voice: manifest.ttsVoice || undefined,
instruction: manifest.ttsInstruction || undefined,
rate: ttsRate,
})
const segment = {
id: segId,
text: segInput.text,
audio: path.relative(dir, filePath).replace(/\\/g, '/'),
estimatedDuration: Math.round(segInput.estimatedDuration * 1000) / 1000,
duration: Math.round(realDuration * 1000) / 1000,
startOffset: Math.round(globalOffset * 1000) / 1000,
}
segments.push(segment)
globalOffset += realDuration
log('tts', `[${idx}/${items.length}] 段${j + 1}: 估算${segInput.estimatedDuration.toFixed(2)}s → 实测${realDuration.toFixed(2)}s | ${segInput.text.slice(0, 15)}...`)
} catch (err) {
log('tts', `[${idx}/${items.length}] 段${j + 1} 合成失败: ${err.message}`)
segments.push({
id: segId,
text: segInput.text,
audio: '',
estimatedDuration: segInput.estimatedDuration,
duration: 0,
startOffset: globalOffset,
error: err.message,
})
globalOffset += segInput.estimatedDuration
}
}
// Step 3: 汇总到 item
const totalAudioDuration = Math.round(globalOffset * 1000) / 1000
item.segments = segments
item.audio = segments[0]?.audio || ''
item.audioDuration = totalAudioDuration
item.segmentCount = segments.length
// Step 4: 时长合规诊断
const ratio = videoDur / totalAudioDuration
if (ratio < 0.9) {
item._timelineWarning = `⚠ audioDur(${totalAudioDuration.toFixed(1)}s) > videoDur(${videoDur}s)ratio=${ratio.toFixed(2)}assemble 将截断`
}
log('tts', `[${idx}/${items.length}] 完成: ${segments.length}段, 总音频${totalAudioDuration.toFixed(1)}s, ratio=${ratio.toFixed(2)}`)
saveManifest(manifestPath, manifest) saveManifest(manifestPath, manifest)
} }
} }
module.exports = { phaseTts } module.exports = { phaseTts, splitIntoAudioSegments }

View File

@@ -34,13 +34,13 @@ const { createAccount } = require('./lib/cmd-create-account')
// 阶段注册表 // 阶段注册表
// ============================================================================ // ============================================================================
const ALL_PHASES = ['images', 'upload', 'videos', 'tts', 'assemble'] const ALL_PHASES = ['tts', 'images', 'upload', 'videos', 'assemble']
const PHASE_HANDLERS = { const PHASE_HANDLERS = {
tts: phaseTts,
images: phaseImages, images: phaseImages,
upload: phaseUpload, upload: phaseUpload,
videos: phaseVideos, videos: phaseVideos,
tts: phaseTts,
assemble: phaseAssemble, assemble: phaseAssemble,
} }
@@ -229,11 +229,12 @@ async function main() {
console.log(' pipeline.js validate --manifest <path>') console.log(' pipeline.js validate --manifest <path>')
console.log(' pipeline.js confirm --manifest <path> --all') console.log(' pipeline.js confirm --manifest <path> --all')
console.log(' pipeline.js confirm --manifest <path> --items 1,3,5') console.log(' pipeline.js confirm --manifest <path> --items 1,3,5')
console.log(' pipeline.js run --manifest <path> [--account id] [--phase p1,p2] [--resume] [--retry-failed]') console.log(' pipeline.js run --manifest <path> --phase tts,images,upload,videos,assemble')
console.log(' pipeline.js run --manifest <path> --resume')
console.log(' pipeline.js status --manifest <path>') console.log(' pipeline.js status --manifest <path>')
console.log('') console.log('')
console.log('Manifest 路径约定: output/{account}_{date}_{NNN}/manifest.json同天自增序号') console.log('Manifest 路径约定: output/{account}_{date}_{NNN}/manifest.json同天自增序号')
console.log('阶段: images, upload, videos, tts, assemble') console.log('阶段: tts → images upload videos assembleTTS提前')
} }
if (require.main === module) { if (require.main === module) {

View File

@@ -108,7 +108,7 @@ function synthesize(text, options = {}) {
format: 'mp3', format: 'mp3',
sample_rate: 24000, sample_rate: 24000,
volume: 50, volume: 50,
rate: options.rate || 1.1, rate: options.rate || 1.15,
pitch_rate: 1.0, pitch_rate: 1.0,
text_type: 'PlainText', text_type: 'PlainText',
...(instruction ? { instruction } : {}), ...(instruction ? { instruction } : {}),

View File

@@ -22,10 +22,62 @@
--- ---
## 执行流程 ## 执行流程(含固定产出表)
**Step 1 — 模式锁定** 每次执行,无论是否讨论细节,**必须输出时间线总表**,不得跳过。
读取【成片模式】,后续所有规则按模式分支执行,不混用。
---
### 流程节点
```
Step 2 — 全文扫描 + 排时间线
↓ 输出「时间线总表 v1」shotDesc 空白,其余按阶段填写)
Step 5 — shotDesc 生成(分镜)
↓ 输出「时间线总表 v2」shotDesc 已填,其余待生成)
[子 Agent 生成画面提示词]
↓ 输出「时间线总表 v3」画面提示词已填视频提示词待生成
[子 Agent 生成视频提示词]
↓ 输出「时间线总表 v4」全部填完
```
---
### 固定产出表(每个节点输出)
每次输出表格,表头不变,格式固定:
```
【时间线总表 vN】 日期YYYY/MM/DD 成片模式:视频成片/图文成片
序号 | 口播文案内容 | 估算时长 | 画面分镜描述shotDesc | 画面提示词 | 视频提示词 | 视频vs音频对比 | 处理误差方式
S1 | [原文逐字] | X.Xs | [待生成/已生成] | [待生成/已生成] | [待生成/已生成] | 视频X.Xs vs 音频X.Xs | [见下方]
S2 | ... | ... | ... | ... | ... | ... | ...
...
```
**「视频vs音频对比」列说明**
- 视频固定长度Kling 6s / VEO ~8s
- 音频估算时长:字数 ÷ 5字/秒)
- 对比结果视频X.Xs **>** 音频X.Xs✅ 余韵)/ 视频X.Xs **≈** 音频X.Xs 紧)/ 音频超视频 → 标红
**「处理误差方式」列(固定写死):**
- 视频 > 音频:余韵留给剪辑衔接,不处理
- 视频 ≈ 音频(差距 < 0.5s装配时视频微速调整±10% 内)
- 音频 > 视频Shot 拆分或与相邻 Shot 合并,不得让视频放慢
- 绝对禁止:音频减速播放
**空缺标记规范:**
- 尚未生成的内容:写「待生成」,不得留白
- 已生成的内容:填写实际内容
- 跨版本保持一致,已填内容不在后续版本中删除或改写
**v1 版只填前三列**(序号 / 口播文案内容 / 估算时长shotDesc 列写「待生成」,其余列写「—」
**v2 版填 shotDesc**,画面提示词写「待生成」,视频提示词写「待生成」
**v3 版填画面提示词**,视频提示词写「待生成」
**v4 版填视频提示词**,全部完成
---
**Step 2 — 全文扫描** **Step 2 — 全文扫描**
通读完整文案,识别: 通读完整文案,识别:
@@ -42,22 +94,34 @@
| `kitano` | 孤独 / 等待 / 沉默对峙 / 留白冲击 / 收尾余韵 | | `kitano` | 孤独 / 等待 / 沉默对峙 / 留白冲击 / 收尾余韵 |
| `fincher` | 规律揭示 / 拆解者视角 / 对称破坏 / 审讯感 / 压迫感 | | `fincher` | 规律揭示 / 拆解者视角 / 对称破坏 / 审讯感 / 压迫感 |
**Step 4 — 切** **Step 4 — 切分计算(自动执行,不讨论)**
*图文成片:* 仅执行语义场景切割。每段覆盖一个完整观点50字左右时长410秒。 规则与参数:
- TTS 语速5 字/秒1.15x 标准语速,不变)
- 视频固定长度Kling 6s / VEO ~8s
- 原则:音频估算时长 < 视频固定长度,视频留余量,不做加速处理
*视频成片:* 先语义场景切割,再对每个场景内部执行气口切割: | 字数范围 | TTS 估算 | 处理方式 |
- 切割点:逗号、自然停顿、从句边界 |---------|---------|---------|
- 每段822字目标1518字 | ≤ 8 字 | ≤ 1.6s | 并入相邻 shot |
- 不足8字合并至相邻段超过22字继续在下一逗号切开 | 924 字 | 1.84.8s | 可单独,余韵留给视频 |
- 连续 Shot 的 script 按序拼接 = 原文一字不差 | 2532 字 | 56.4s | 单独成 shot推荐区间 |
| 3348 字 | 6.69.6s | 必须拆成 2 个 shot |
| ≥ 49 字 | ≥ 9.8s | 拆成 3 个 shot |
- 优先按语义单元合并,跨语义不合并
- 合并后落在 2532 字区间则停止合并
**Step 5 — shotDesc 生成** **Step 5 — shotDesc 生成**
每条 Shot 按模式规则写 shotDesc见§参考规则执行前自检。 每条 Shot 按模式规则写 shotDesc见§参考规则执行前自检。
输出时间线总表 v2shotDesc 已填)。
**Step 6 — 输出** **Step 6 — 输出**
先输出总览行,再输出 JSON。 先输出总览行,再输出 JSON。
**Step 1 — 模式锁定**
读取【成片模式】,后续所有规则按模式分支执行,不混用。
--- ---
## 输出格式 ## 输出格式
@@ -87,16 +151,19 @@
- [ ] 这帧图片独立存在时,用户能被画面吸引吗?→ 否则重写 - [ ] 这帧图片独立存在时,用户能被画面吸引吗?→ 否则重写
- [ ] 与上一帧景别/构图/视角是否有对比变化?→ 连续同景别禁止 - [ ] 与上一帧景别/构图/视角是否有对比变化?→ 连续同景别禁止
**视频成片:** **视频成片JSON 完成后逐 Shot 自评,缺一不可)**
- [ ] 视频模型知道往哪个方向动吗?→ 否则重写(必须含隐性动势词 - [ ] 冲突强度A / B / C出现 C 必须强化D/F 直接重写
- [ ] 视觉锚点A / B / C出现 D/F 直接重写)
- [ ] 动态张力A / B / C出现 D/F 直接重写)
- [ ] 概念对应A / B / C出现 D/F 直接重写)
- [ ] 画面冲击度A / B第一个 Shot 必须 A/BC 直接重写)
- [ ] 第一个 Shot 冲突强度是否为 A→ 否则整组重做
- [ ] script 拼接后等于原文吗?→ 否则重做气口切割 - [ ] script 拼接后等于原文吗?→ 否则重做气口切割
- [ ] shotDesc 里出现的每个动作主体,在当前 script 里都能找到对应吗?→ 找不到则删除
**全局JSON 完成后执行):** **全局JSON 完成后执行):**
- [ ] 纯物体/局部特写超过 Shot 总数的一半?→ 替换为有脸的人物场景
- [ ] 有连续两个 Shot 都是纯物体/局部特写?→ 插入人物上半身场景
- [ ] 有双人同框互动?→ 改为单人物 + 环境暗示(空椅/阴影/门后暗示)
- [ ] 有微表情描写(瞳孔/嘴角微颤/汗珠等)?→ 改为大动作或环境变化
- [ ] 有依赖跨 Shot 角色连续性的叙事?→ 让每个 Shot 独立成立 - [ ] 有依赖跨 Shot 角色连续性的叙事?→ 让每个 Shot 独立成立
- [ ] 画面主体选人物还是物件,依据是否为"旁白核心论点需要人物"?→ 若旁白是概念/规律陈述,优先选物件
--- ---
@@ -108,13 +175,34 @@
主体 + 状态/姿态 + 环境 + 构图张力(空间关系/视觉隐喻/情绪重量) 主体 + 状态/姿态 + 环境 + 构图张力(空间关系/视觉隐喻/情绪重量)
**视频成片3060词** **视频成片3060词**
主体 + 状态/姿态(必须有动态倾向)+ 环境 + **隐性动势(必填)** 主体 + 状态/姿态(必须有动态倾向)+ 环境 + **动作起点(必填)**
### 隐性动势词库 ### 动作起点原则
人物:`slowly turning head` / `jaw tightening` / `eyes narrowing toward edge of frame` / `shoulders barely shifting forward` / `fingers tightening` / `exhaling a controlled breath` / `gaze drifting downward with weight` / `lips pressing into a hard line` / `knuckles whitening` / `thumb pressing with slow increasing pressure` 选取与当前旁白语义最相关的人物动作或场景变化,动作必须有可见的起点状态和终点状态,幅度肉眼可见。优先从旁白动词中提取动作意象,将其字面化为画面中正在发生的动作,而非用光影氛围替代。
场景:`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` 旁白为概念型或描述型(无明确动词,如"权力从来不大声说话""代价是由别人来付的")时:从旁白核心张力中提取一个能将该张力物理化的象征物,以象征物完成单一完整的状态变化作为动作起点——象征物的变化本身即论点,不用人物动作替代。
### 文案-画面编码原则
shotDesc 必须从当前 script 的核心行为意象出发设计画面,
禁止生成与文案内容无关的通用氛围画面。
自检方法:只看 shotDesc能否直接感受到这段文案在说什么
如果不能,则重写。
示例对照:
✗ 错误:文案"不在对话里拆自己的台"
→ shotDesc"男子笔直站立直视镜头"
(通用权谋画面,与文案行为意象无关)
✓ 正确:文案"不在对话里拆自己的台"
→ shotDesc"男子在说话中途身体重心向后转移,
已经开口的姿势被自己的退步打断,脚跟抬起后又
落回原位,话还没说完,整个上半身已经往后挪了
半个身位,手里的笔被无意识地放下"
(文案核心行为被直接编码进画面,禁止使用面部微
表情,只用肉眼可见的身体位移和物体变化)
### shotDesc 禁止事项 ### shotDesc 禁止事项
- 禁止镜头运动参数zoom-in / pan - 禁止镜头运动参数zoom-in / pan
@@ -123,6 +211,36 @@
- 禁止提前使用后续文案才出现的意象(剧透) - 禁止提前使用后续文案才出现的意象(剧透)
- 视频成片禁止纯静止描述 - 视频成片禁止纯静止描述
### 吸引力维度(每个 shot 必须自评,不得低于 C
**五维评分标准:**
| 维度 | A | B可接受 | C勉强 | D/F不合格 |
|------|--------|-----------|---------|--------------|
| **冲突强度** | 画面有明确的冲突或矛盾(两个人/两股力量/压迫与抵抗/封闭与突破) | 有潜在冲突但表现间接 | 画面平和,无冲突感 | 纯静态陈述,无任何张力 |
| **视觉锚点** | 第一眼就能锁定一个具体物件/细节,视觉冲击力强 | 有锚点但不够突出 | 锚点模糊,主体不明确 | 没有锚点,画面散乱 |
| **动态张力** | 动作弧清晰,观众想看下一秒会发生什么 | 有动作但幅度小,悬念弱 | 动作存在但不可见/不可信 | 纯静止 |
| **概念对应** | 画面直击文案核心论点,看完就知道这段在讲什么 | 画面与文案有关联,需思考才能对应 | 画面与文案关联弱,需对照才能理解 | 画面与文案脱节 |
| **画面冲击度** | 构图/光影/物件有令人不安或被吸引的瞬间 | 有冲击力但不够强 | 冲击力一般 | 平淡无奇,缺乏记忆点 |
**Shot 开头的特殊要求:**
- 第一个 Shot 的**冲突强度**必须为 A
- 第一个 Shot 的**画面冲击度**必须为 A 或 B
- 开头禁止纯人物走路/站立/坐着等无冲突开场
**自评规则:**
- 每输出一个 shot自评每个维度的等级A/B/C/D
- 任何维度出现 D → 该 shot 必须重写
- 冲突强度出现 C → 必须强化冲突设计
- 第一个 Shot 出现非 A 冲突强度 → 整组分镜打回重做
**冲突来源(选取与当前旁白语义最相关的):**
- 空间压迫(低顶/窄门/堆满杂物的角落/人在边缘)
- 光影对峙(强光切割阴影/明暗分界线在主体身上)
- 物件隐喻(破碎/倾倒/合拢/打开/压低/撑起)
- 身体姿态(对抗重力/突破边界/被迫收缩)
- 权力位置(谁在画框中央/谁被裁切/谁在视线外)
### 导演构图速查 ### 导演构图速查
**Tarantino** 局部极端特写(手/脚/颈/眼)/ 对话权力位置(谁背对镜头/谁被仰拍)/ 日常感的危险 / 仰拍视角 / 画框内画框 **Tarantino** 局部极端特写(手/脚/颈/眼)/ 对话权力位置(谁背对镜头/谁被仰拍)/ 日常感的危险 / 仰拍视角 / 画框内画框
@@ -131,11 +249,21 @@
**Fincher** 近乎对称但被破坏 / 几何负空间(阴影/柱子/桌角形成精确边界)/ 上帝视角凝视 / 精确景深层次 / 一个错位物件承载叙事 **Fincher** 近乎对称但被破坏 / 几何负空间(阴影/柱子/桌角形成精确边界)/ 上帝视角凝视 / 精确景深层次 / 一个错位物件承载叙事
### 人间感规 ### 视觉主体选择原
1. 至少一半 Shot 包含完整人物(有脸、有上半身、能看出性别和大概年龄)
2. 禁止连续两个 Shot 都是纯物体或局部特写 **人物 vs 非人物的选择依据:**
3. shotDesc 优先从「具体人物在具体场景中的具体行为」出发 - 旁白以"人"为核心论点(讲人的行为/选择/代价)→ 设计含人物的画面
4. 涉及「判断/审视/压制」等权力关系时,优先设计眼神对峙或回避构图,而非物体隐喻 - 旁白以"概念/规律"为核心(讲道理/拆解现象/结论陈述)→ 优先设计物件/空间/光影承载叙事
- 禁止用人物动作演绎旁白里没有的行为
**常见误区:**
- 旁白讲"开源节流",画面生成一个人做财务动作 → 人云亦云,无视觉冲击
- 旁白讲"跨越阶层",画面生成一个人走路/站起来 → 通用演绎,无冲突
**正确做法:**
- 找到旁白的核心冲突(压迫 vs 抵抗 / 封闭 vs 突破 / 消耗 vs 积累)
- 将冲突具象化为一个视觉物件或空间关系
- 用物件/空间的变化弧替代人物表演
### 语义-画面对齐三定律 ### 语义-画面对齐三定律
- **禁止剧透:** 不使用当前 script 之后才出现的意象 - **禁止剧透:** 不使用当前 script 之后才出现的意象

View File

@@ -0,0 +1,7 @@
[
{"id":1,"shotDesc":"A man stands alone in the center of a dim room with bare concrete walls, his back straight and head still, eyes staring directly into camera with a subtle jaw clench, the camera is static and centered, symmetric framing with slight negative space above, conveying compressed authority.","script":"一个人越来越自信的前提,就是永远不在对话里拆自己的台。","duration":6,"directorRef":"fincher","keyword":"压迫感"},
{"id":2,"shotDesc":"A man sits in the corner of a dim room, shoulders slumping forward as his head tilts slightly downward, a cracked mirror on the wall behind him reflects his fragmented silhouette, the camera remains static framing the hunched figure against the broken reflection.","script":"很多人在关系里为了回避冲突,会通过主动贬低自己,替别人铺好台阶。","duration":6,"directorRef":"fincher","keyword":"自我贬低"},
{"id":3,"shotDesc":"Close-up of rough hands sliding playing cards across a dark desk surface, fingers pressing the cards flat against the wood, the camera is static and centered framing the hands in rigid symmetry, a thin shadow cuts through the center of frame.","script":"你会在表达观点前说一句我可能想的不太对,","duration":5,"directorRef":"fincher","keyword":"权谋张力"},
{"id":4,"shotDesc":"A man leans forward at a narrow desk in a dim room, one hand pressing flat on the surface, his head bowed and jaw tight, gaze cast downward, the camera is static and slightly elevated capturing the slight forward lean from a side angle, the figure fills the right side of frame leaving negative space on the left.","script":"会在提出需求时补一句,如果不方便也没关系,","duration":5,"directorRef":"fincher","keyword":"隐忍让步"},
{"id":5,"shotDesc":"A man sits alone in an empty room, his body slightly curled inward, gaze turned deliberately away from the camera, the camera is static framing the averted posture, the left third of frame is empty floor, conveying tension through avoidance rather than confrontation.","script":"还会在取得成果后赶紧解释,我只是运气好。","duration":5,"directorRef":"fincher","keyword":"自我否定"}
]

View File

@@ -8,7 +8,25 @@
重要前提:你生成的图片是下游视频片段的起始帧。构图和姿态必须是「即将发生」的瞬间,而非「已完成」的状态。 重要前提:你生成的图片是下游视频片段的起始帧。构图和姿态必须是「即将发生」的瞬间,而非「已完成」的状态。
## 二、入参说明与权重关系(严格遵守) ## 二、固定产出要求
**每次执行必须输出时间线总表**,不得跳过。
完成后输出:
```
【时间线总表 v3】 日期YYYY/MM/DD 成片模式:视频成片/图文成片
序号 | 口播文案内容 | 估算时长 | 画面分镜描述shotDesc | 画面提示词 | 视频提示词 | 视频vs音频对比 | 处理误差方式
S1 | [原文逐字] | X.Xs | [已生成] | [已生成内容] | 待生成 | 视频X.Xs vs 音频X.Xs | [固定规则]
S2 | ... | ... | ... | ... | ... | ... | ...
```
- 已填内容保持不变,后续版本不覆盖
- 视频提示词列统一写「待生成」,由视频提示词阶段填写
- 视频vs音频对比列视频固定长度 / 音频估算时长,字 ÷ 5
## 三、入参说明与权重关系(严格遵守)
| 参数 | 角色 | 使用规则 | | 参数 | 角色 | 使用规则 |
|-----|------|---------| |-----|------|---------|
@@ -117,7 +135,7 @@ precise depth separation, cold exact even illumination
``` ```
[情绪定性词] + [情绪定性词] +
[主体描述 + 隐性动势(来自 shotDesc完整保留] + [主体描述 + 动作起点(来自 shotDesc完整保留] +
[环境/背景(来自 shotDesc] + [环境/背景(来自 shotDesc] +
[光影渲染(来自 directorRef 对应词库)] + [光影渲染(来自 directorRef 对应词库)] +
[固定账号画风词尾] + [固定账号画风词尾] +

View File

@@ -8,26 +8,55 @@
重要前提静态分镜图是视频的起始帧。videoPrompt 必须从这帧图的状态出发设计运动,不得重新设计画面内容。 重要前提静态分镜图是视频的起始帧。videoPrompt 必须从这帧图的状态出发设计运动,不得重新设计画面内容。
## 二、入参说明与权重关系(严格遵守) ## 二、固定产出要求
**每次执行必须输出时间线总表**,不得跳过。
完成后输出:
```
【时间线总表 v4】 日期YYYY/MM/DD 成片模式:视频成片/图文成片
序号 | 口播文案内容 | 估算时长 | 画面分镜描述shotDesc | 画面提示词 | 视频提示词 | 视频vs音频对比 | 处理误差方式
S1 | [原文逐字] | X.Xs | [已生成] | [已生成内容] | [已生成内容] | 视频X.Xs vs 音频X.Xs | [固定规则]
S2 | ... | ... | ... | ... | ... | ... | ...
```
- 全部列填满,此为最终版本
- 视频vs音频对比视频固定长度 vs 音频估算时长(字 ÷ 5
- 处理误差方式:
- 视频 > 音频:余韵留给剪辑衔接,不处理
- 视频 ≈ 音频(差距 < 0.5s装配时视频微速调整±10% 内)
- 音频 > 视频Shot 拆分或与相邻 Shot 合并,不得让视频放慢
- 绝对禁止:音频减速播放
## 三、入参说明与权重关系(严格遵守)
| 参数 | 角色 | 使用规则 | | 参数 | 角色 | 使用规则 |
|-----|------|---------| |-----|------|---------|
| **shotDesc** | 主内容 / 画面硬边界 | 定义画面里有什么、人物姿态、环境;运动设计从 shotDesc 的隐性动势出发并放大;不得替换场景或重新设计人物 | | **shotDesc** | 主内容 / 画面硬边界 | 定义画面里有什么、人物姿态、环境; shotDesc 的动作起点为起始状态,结合当前旁白语义,设计一个有明确起点和终点的可见动作弧,动作弧必须在片段时长内完成;不得替换场景或重新设计人物 |
| **当前旁白script** | 主内容 / 运动的灵魂 | 与 shotDesc 共同构成运动主线;提取情绪节奏 → 对应运动的快慢;提取关键动词和意象 → 转化为具体画面动势;提取暗喻/比喻 → 转化为视觉运动设计 | | **当前旁白script** | 主内容 / 运动的灵魂 | 与 shotDesc 共同构成运动主线;提取情绪节奏 → 对应运动的快慢;提取关键动词和意象 → 转化为具体画面动势;提取暗喻/比喻 → 转化为视觉运动设计 |
| **完整文案** | 仅氛围参考 / 不影响画面内容 | 仅用于理解整体氛围、情绪浓度和核心主题;**禁止将其他段落的意象、物件、动作引入当前片段** | | **完整文案** | 仅氛围参考 / 不影响画面内容 | 仅用于理解整体氛围、情绪浓度和核心主题;**禁止将其他段落的意象、物件、动作引入当前片段** |
| **directorRef** | 运动风格 / 向下游透传 | 由上游分镜指定,本层只执行运动节奏层;不改变 shotDesc 的画面内容只改变运动如何发生可选值tarantino / kitano / fincher | | **directorRef** | 运动风格 / 向下游透传 | 由上游分镜指定,本层只执行运动节奏层;不改变 shotDesc 的画面内容只改变运动如何发生可选值tarantino / kitano / fincher |
运动来源优先级:当前旁白意象 > shotDesc 隐性动势 > directorRef 运动模板 运动来源优先级:旁白动词字面化 > shotDesc动作起点 > 导演主体运动模板
从当前旁白里找运动的理由,导演风格是执行方式,不是内容来源 镜头运动由 AI 模型自行决定,提示词中不写具体镜头指令
画面内容以参考图为准videoPrompt 只描述运动,不重描画面
## 三、账号视觉运动基础风格(固定,不因导演而变 ## 三、账号视觉运动基础风格(AI视频模式适用版
运动风格基调: 运动风格基调Kling/VEO 友好版)
- 克制优于激烈:动作幅度小但有重量 - **持续优于静止:每个片段必须有持续的物理变化,禁止"定格"或"静止"**
- 慢优于快:情绪要「沉进去」,而非「冲出来」 - 有位移优于纯微动作:优先设计身体重心位移、物体位置变化、空间关系重组
- 张力优于美感:宁可压迫不适,也不要流畅唯美 - 环境互动:主体的动作必须有环境响应(尘埃扬起、阴影伸缩、光线变化)
- 隐忍优于爆发:整个片段保持「爆发前夕」的状态 - 节奏:从动作起点的第一帧就开始运动,不要写"画面从X开场"这会让AI先生成静态图
- 结尾留余势:永远不以"定格""静止""收尾"结束,结尾必须是运动进行中的某一刻
**对可灵Kling的特殊约束**
- 禁止用"画面从X开场"作为 Prompt 开头,改用"镜头里,..."或"画面中,..."
- 主体运动必须包含空间位移或形态明显变化(纯面部/手部微变化在 Kling 里几乎不动)
- 结尾永远不给"定格在X",改为"某动作正在发生的中间状态"
## 四、导演主体运动词库(视频层专用) ## 四、导演主体运动词库(视频层专用)
@@ -75,19 +104,11 @@
## 五、三层运动设计(核心,至少覆盖两层) ## 五、三层运动设计(核心,至少覆盖两层)
### 5.1 主体运动层(来自 shotDesc 隐性动势放大,最高优先级) ### 5.1 主体运动层( shotDesc 动作起点为起始状态,设计完整动作弧,最高优先级)
词库: 运动素材来源:先从当前旁白中提取动词和动作意象,将其字面化为画面运动;旁白有明确动词时(如推、摔、站起、关门、落下),优先将该动词直接转化为主体的可见动作,而非从构图张力推导运动方向。
``` 旁白为概念型或描述型(无明确动词)时:不强行提取动词,改为设计一个象征物完成单一完整的物理状态变化,变化本身即论点。象征物选取原则:找到能将旁白核心张力"物理化"的具体物件让它从A状态走到B状态整个过程在片段时长内完成。
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 / a crack in the surface slowly widening / iron shards
breaking loose and falling / fabric shifting as the body beneath goes still /
shadows creeping across a face, consuming it by degrees
```
### 5.2 环境运动层(账号通用) ### 5.2 环境运动层(账号通用)
@@ -124,10 +145,11 @@ slow zoom in / static shot / close-up on [body part]
格式模板: 格式模板:
``` ```
画面从[起始帧状态,与分镜图完全对齐]开始 画面以参考图为起始帧
[主体运动:具体身体部位/物体的动作变化,使用导演对应运动词库]。 [主体运动:从参考图状态出发,走到什么终点,
[环境运动:背景动态元素变化]。 只描述运动,不重复描述画面内容]。
[情绪氛围收尾:片段结尾的状态和余势]。 [环境运动:什么在变化]。
[片段结尾的余势]。
竖版9:16画幅无字幕无水印。 竖版9:16画幅无字幕无水印。
``` ```
@@ -141,12 +163,12 @@ slow zoom in / static shot / close-up on [body part]
格式模板: 格式模板:
``` ```
Opening on [起始帧状态,描述画面里有什么]. Opening with the reference image as the starting frame.
[主体运动:subject的具体动作变化使用导演对应运动词库]. [主体运动:from the reference image state to a clear endpoint,
[环境运动:背景元素的变化]. describe only motion, do not re-describe the scene].
[情绪收尾状态]. [环境运动what is changing].
aspect ratio 9:16, no text overlay, no subtitles, [片段结尾的余势].
24fps, cinematic. aspect ratio 9:16, no text overlay, no subtitles, 24fps, cinematic.
``` ```
VEO 专属注意: VEO 专属注意:
@@ -165,8 +187,9 @@ VEO 专属注意:
格式模板: 格式模板:
``` ```
[自然语言完整描述:起始状态 → 主体动作 [自然语言完整描述:以参考图为起始帧,
→ 环境变化 → 结尾余势,流畅的叙述性语言]. 主体从画面当前状态走向终点——只描述运动,
不重复描述画面内容]。
Vertical format 9:16, cinematic, no text. Vertical format 9:16, cinematic, no text.
``` ```
@@ -192,7 +215,7 @@ Vertical format 9:16, cinematic, no text.
- 主体运动:[具体描述,含身体部位/物体,使用导演对应词库] - 主体运动:[具体描述,含身体部位/物体,使用导演对应词库]
- 环境运动:[具体描述] - 环境运动:[具体描述]
- 镜头运动:由模型自行决定 - 镜头运动:由模型自行决定
**动势继承:** [shotDesc隐性动势] → [视频中的放大演绎] **动势继承:** [shotDesc动作起点] → [视频中的完整动作弧]
**videoPrompt** **videoPrompt**
[完整提示词,可直接复制使用] [完整提示词,可直接复制使用]
**剪辑衔接:** **剪辑衔接:**
@@ -256,6 +279,167 @@ shotDesc「knuckles beginning to whiten」
自然衔接下一镜头的「今天逐帧拆」揭示 自然衔接下一镜头的「今天逐帧拆」揭示
``` ```
## 示例B旁白含强动作动词型
**输入:**
```
【shotDesc】
a lone figure seated at the far edge of a low wooden bench,
weight tilting forward onto both palms, spine beginning its
upward extension — the body already committing to a rise
it has not yet completed.
【当前旁白】有一天他终于站起来,头也不回地走了。
【完整文案】
他在那个房间里坐了三年。有一天他终于站起来,头也不回地走了。
门关上的声音,是他给那段关系最后的句号。
【时长】6s
【directorRef】fincher
【目标模型】Kling
```
**输出:**
```
### Shot 02 视频提示词 | 6s | Fincher | Kling
**叙事意图:**
转折核心——旁白"站起来走了"是整段文案的动作句,
视频必须将这个字面动作完整执行,不能用氛围替代
**运动设计:**
- 主体运动:人物从倾身坐姿完成起立全过程,
以恒定速度站直,随即身体转向,背对镜头迈步离开,
整个动作弧在6秒内完整完成
- 环境运动:人物离开后,空置的木凳留在画面中央,
凳面投影随光线缓慢延伸
- 镜头运动:由模型自行决定
**动势继承:**
shotDesc「spine beginning its upward extension」
→ 视频中:脊背完成延伸,站立,转身,离开,
最终画面定格在空凳
**videoPromptKling**
画面从人物倾身坐于长凳边缘开场,
双掌撑在膝上,身体重心已前移,
脊背开始向上延伸。
人物以恒定速度站起,不停顿,不回头,
身体转向后迈步向画面深处走去。
走出画面后,长凳独自留在原位,
凳面投影在空旷中缓慢延伸。
竖版9:16画幅无字幕无水印。
**剪辑衔接:**
- 片段开头:人物倾身坐姿,与静态分镜图完全对齐
- 片段结尾:空凳定格,余势为「门即将关上」,
自然衔接下一镜头的关门声
```
## 示例C物品主导运动型
**输入:**
```
【shotDesc】
a heavy wooden door at the far end of a long corridor,
standing slightly ajar — a thin vertical line of dim light
visible at its edge, the door's shadow stretching the full
length of the floor toward the viewer.
【当前旁白】那扇门从那一天起,就再也没开过。
【完整文案】
他在那个房间里坐了三年。有一天他终于站起来,头也不回地走了。
门关上的声音,是他给那段关系最后的句号。
【时长】5s
【directorRef】kitano
【目标模型】Kling
```
**输出:**
```
### Shot 03 视频提示词 | 5s | Kitano | Kling
**叙事意图:**
收尾句——"再也没开过"是终结性陈述,
画面由物品(门)主导完整运动弧,
人物不出现,门的关闭即是全部叙事
**运动设计:**
- 主体运动:门从虚掩状态缓慢完成关闭,
光线随门缝收窄而消失,
最终画面变为门关闭后的完全静止
- 环境运动:地板上的门影随门的移动
缓慢压缩,最终消失
- 镜头运动:由模型自行决定
**动势继承:**
shotDesc「thin vertical line of dim light at its edge」
→ 视频中:那条光线从有到无完整消失,
门完成关闭,静止定格
**videoPromptKling**
画面从走廊尽头虚掩的木门开场,
门缝处一条细窄光线垂直落在地板上,
门影向镜头方向延伸。
门以极缓的速度向关闭方向移动,
光线随门缝收窄逐渐消失,
地板上的门影同步压缩。
门完成关闭,光线彻底消失,
画面在完全静止中定格。
竖版9:16画幅无字幕无水印。
**剪辑衔接:**
- 片段开头:走廊尽头虚掩门,与静态分镜图完全对齐
- 片段结尾:门关闭后静止定格,
余势为「句号落下」,无需衔接后续
```
## 示例D概念型旁白·意象隐喻型
**输入:**
```
【shotDesc】
a single scale — one side weighted down to the table,
the other lifted high, frozen at its maximum tilt.
On the lowered side, a small smooth stone. On the raised
side, nothing visible — only empty air holding the balance.
【当前旁白】代价从来都在,只是由谁来付的问题。
【完整文案】
权力从来不大声说话。它藏在一个人坐在哪里,看向哪里,
在哪句话之后沉默了三秒。代价从来都在,只是由谁来付的问题。
【时长】5s
【directorRef】fincher
【目标模型】Kling
```
**输出:**
```
### Shot 04 视频提示词 | 5s | Fincher | Kling
**叙事意图:**
概念落地——"代价由谁来付"是抽象论点,
无明确动词可字面化,
用天平的物理状态变化将论点变得可见
**运动设计:**
- 主体运动:天平压低的一端以恒定速度继续下压,
直到触碰桌面并定格;
另一端同步升至最高点后静止,
整个过程匀速不间断,像一个已经决定的结局在发生
- 环境运动:桌面阴影随天平倾角变化精确移动,
最终在定格瞬间形成最大阴影面积
- 镜头运动:由模型自行决定
**动势继承:**
shotDesc「frozen at its maximum tilt」
→ 视频中:从"最大倾角冻结"继续走完——
压低端触底,升高端到顶,完整完成这个物理判决
**videoPromptKling**
画面从天平开场,一端已被压低,
另一端高悬,两端之间的倾角被冻结在最大值。
压低的一端以恒定速度继续下压,
不加速不减速,像一台已启动就不会停下的机器。
压低端触碰桌面,发出无声的定格。
升高端同步到达最高点并静止。
桌面阴影随倾角变化精确移动,
在定格瞬间铺展到最大面积。
竖版9:16画幅无字幕无水印。
**剪辑衔接:**
- 片段开头:天平最大倾角冻结状态,与静态分镜图完全对齐
- 片段结尾:天平两端同时定格在极值,
余势为「判决已下,无需解释」,自然收尾或衔接下一段论点
```
## 十、质量自检清单 ## 十、质量自检清单
- 起始状态与静态分镜图完全匹配 - 起始状态与静态分镜图完全匹配
@@ -269,9 +453,10 @@ shotDesc「knuckles beginning to whiten」
- 允许承接:运动可以延续前一片段的动势方向 - 允许承接:运动可以延续前一片段的动势方向
- 运动幅度符合账号「隐忍张力」基调 - 运动幅度符合账号「隐忍张力」基调
- 片段结尾留有余势 - 片段结尾留有余势
- 主体运动是否有空间位移或形态的可见变化?纯面部或手部微变化不计入——若画面主体本身就是物体(旗帜/武器/车辆),则该物体的形态变化(如折叠/展开)可以计入;若没有符合条件的变化,替换为人物重心转移 / 物体位置改变 / 空间关系重组,三选一
- 语言和参数格式与目标模型匹配 - 语言和参数格式与目标模型匹配
- 视频第一帧 = 静态分镜图状态,对不上则整个片段脱锚 - 视频第一帧 = 静态分镜图状态,对不上则整个片段脱锚
directorRef 影响主体运动方式,画面内容始终来自 shotDesc directorRef 影响主体运动方式,画面内容始终来自 shotDesc
运动来源优先级:旁白意象 > shotDesc隐性动势 > 导演主体运动模板 运动来源优先级:旁白动词字面化 > shotDesc动作起点 > 导演主体运动模板
镜头运动由 AI 模型自行决定,提示词中不写具体镜头指令 镜头运动由 AI 模型自行决定,提示词中不写具体镜头指令

View File

@@ -0,0 +1,74 @@
{
"id": "执黑先行",
"name": "执黑先行",
"description": "执黑先行账号,现代都市数字插画风,半写实,漫威色调,冷暖双光源对抗",
"pipeline": "image-video",
"defaultFormat": "9:16",
"imageModel": "gemini",
"videoModel": "kling",
"batchSize": 30,
"styles": {
"执黑先行": {
"references": []
}
},
"ttsVoice": "cosyvoice-v3.5-plus-bailian-fa8787c0f70b4ba2a907c35511e6a6f6",
"ttsInstruction": "用沉稳有力的男性声音朗读,语速适中,语气坚定有力,像是一个有经历有力量的人在平静地讲述生活的方向",
"storyboardPrompt": "prompts/分镜.md",
"imageStylePrompt": "prompts/图片提示词.md",
"videoStylePrompt": "prompts/视频提示词.md",
"capcut": {
"effects": [],
"filter": "自然日光:30",
"subtitleStyle": {
"font": "思源黑体 Heavy",
"fontSize": 24,
"color": "#FFFFFF",
"highlightColor": "#F5A623",
"bold": true,
"hasShadow": true,
"shadowColor": "#3D2B1F",
"shadowAlpha": 0.6,
"transformY": -380,
"alignment": 1,
"inAnimation": "淡入",
"outAnimation": "淡出"
},
"keywordStyle": {
"textEffect": "简约白色黑边花字",
"fontSize": 60,
"color": "#F5A623",
"bold": true,
"transformY": 0,
"inAnimation": "打字机效果",
"outAnimation": "模糊淡出",
"inAnimDuration": 300000,
"outAnimDuration": 300000
},
"defaultBGM": "",
"transitions": {
"strategy": "rhythm",
"default": { "name": "叠化", "duration": 200000 },
"byPosition": {
"hook": { "name": "叠化", "duration": 200000 },
"keypoint": { "name": "叠化", "duration": 250000 },
"body": { "name": "叠化", "duration": 300000 },
"closing": { "name": "叠化", "duration": 400000 }
},
"byDirector": {
"tarantino": { "name": "叠化", "duration": 250000 },
"kitano": { "name": "叠化", "duration": 300000 },
"fincher": { "name": "叠化", "duration": 200000 }
}
},
"kenBurns": {
"enabled": true,
"default": { "startScale": 1.0, "scaleRate": 0.9, "panXRate": 0, "panYRate": 0 },
"byDirector": {
"tarantino": { "startScale": 1.0, "scaleRate": 0.85, "panXRate": 0, "panYRate": 0 },
"kitano": { "startScale": 1.03, "scaleRate": 0.5, "panXRate": 0, "panYRate": 0 },
"fincher": { "startScale": 1.0, "scaleRate": 0.4, "panXRate": 0, "panYRate": 0 }
}
}
}
}

View File

@@ -0,0 +1,306 @@
# 分镜脚本生成 Agent v5执黑先行账号专版
## 输入格式
```
【口播文案】{完整文案原文}
【成片模式】图文成片 | 视频成片
```
若任一字段缺失,返回错误并终止:`ERROR: 缺少必要字段 [口播文案 | 成片模式]`
---
## 风格锚定(内部执行,不输出)
**画风定义:** 现代都市时尚插画风,半写实,有质感,好看是第一优先级
**画面基调跟着文案走:** 文案轻则活泼明亮,文案重则沉稳有力,不预设固定基调
**禁止出现:** 土气/邋遢/无造型感/真实人物姓名
**音频时间线(固化铁律,写死在 qwen-tts.js**
- TTS 语速 = **正常语速的 1.15 倍**(不可修改)
- 文案总音频估算时长 = 文案总字数 ÷ 5字/秒)
- 音频长度是时间轴主轴,视频配合音频;**音频不可调速、不可加速/放缓**
- CapCut 导入音频时无 speed 字段,以 1.15x 原始速率直接进入时间线
**视频时间线(固化铁律):**
- Kling 可灵视频片段固定 **6 秒**(实测恒定,不可配置)
- **每个 shot 的 TTS 估算script字数÷5必须 ≤ 6 秒**
- audioDur > videoDur 的 shot 必须在分镜阶段拆分assemble 不允许慢放/冻结
- 视频适配策略:比音频长 → 加速(speed_up) 或截断(trim);比音频短 → 禁止
**人物锚点(固化规则,不可跳过):**
所有出现的人物,必须参照以下两个方向,生成有视觉魅力和吸引力的都市形象:
**男性人物锚点 → Spider-Man 动漫版造型**
参考Spider-Man: Into the Spider-Verse数字插画版
特征:线条利落 / 下颌轮廓清晰 / 肩宽胸厚 / 站姿挺拔有力 / 表情克制有焦点 / 眼神锐利或深邃 / 漫感数字插画面孔,非真人感
禁止:发福/无轮廓/网红感/表情夸张/咧嘴笑/驼背/肩塌
**女性人物锚点 → 现代版赫本**
参考Audrey Hepburn 形象翻译至现代都市版本
特征:面部留白感 / 眉峰平缓干净 / 气质克制 / 眼神平静有内容 / 妆容有辨识度(自然系眉毛+简约眼妆)/ 体态端庄 / 穿搭有质感不暴露
禁止:浓妆/网红感/暴露穿着/夸张表情/咧嘴笑/姿态松垮
**人物统一要求(固化):**
- 下颌线必须清晰(男女通用)
- 肩线必须干净挺拔(禁止肩塌/缩肩)
- 眼神必须有焦点(禁止空洞/涣散/无焦点)
- 表情克制(禁止大笑/夸张表情)
**人物方向示例(固化规则):**
- 任何身份(师傅/中年人/学生/街头少年)→ 统一做成 Spider-Verse 漫感都市男,有型有张力
- 任何身份(师傅/中年女性/学生妹)→ 统一做成现代赫本风都市女,优雅克制有气质
好看是第一优先级,造型跟着内容走,但必须有性张力参照系。
---
## 执行流程
**Step 1 — 模式锁定**
读取【成片模式】,后续所有规则按模式分支执行,不混用。
**Step 2 — 全文扫描**
通读完整文案,识别:
- 语义场景数量(情绪转折 / 场景转换 / 主体变化 / 节奏重音)
- 整体气质 → 锁定导演,全程统一
**Step 3 — 导演锁定**
根据文案整体气质选定一位导演,**全部 Shot 统一使用,不中途切换**
| 导演 | 适配气质 |
|------|---------|
| `tarantino` | 冷暖对抗 / 实用光源 / 硬边阴影 / 力量感强 |
| `kitano` | 冷调孤独 / 大面积暗部 / 都市感 / 克制与沉默 |
| `fincher` | 精确计算 / 冷蓝实用光 / 建筑感阴影 / 揭示感 |
**Step 4 — 时长规划与语义切割(固化,不可跳过)**
时间线规则(固化):
- 中文语速 1.15x ≈ **5 字/秒**(固化)
- Kling 可灵单片段上限:**6 秒**(固化,是硬上限,不可超过)
- 文案是时间轴锚点:总音频时长 = 文案总字数 ÷ 5
- 音频是主时间线,视频配合音频
TTS 估算 = 文案字数 ÷ 5
| TTS 估算 | 决策 |
|---------|------|
| < 3 秒 | 强制合并至相邻 Shot |
| 35 秒 | 优先合并至相邻 Shot |
| 56 秒 | 可单独,余韵可接受 |
| 612 秒 | 强制拆成 2+ 个 Shot每个 ≤ 6s |
| > 12 秒 | 按语义自然停顿拆成多 Shot确保每段 ≤ 6s |
**核心原则:每个 shot 的 script 内容必须与时长匹配。不允许用重复完整句凑时长!**
合并后 script = 原文字符串拼接,一字不差。
**Step 5 — shotDesc 生成**
每条 Shot 按模式规则写 shotDesc见§参考规则
**shotDesc 核心要求:**
- 必须直接体现文案的核心动作或核心冲突
- 禁止只写姿态、状态、表情——没有动作的 shotDesc 在视频层无效
- 人物一律时尚有型urban fashion / streetwear / modern clothing
**Step 6 — 五维自评**
每个 Shot 自评五维(见§吸引力维度),不合格打回重写。
**Step 7 — 输出**
先输出总览行含TTS规划表再输出 JSON。
---
## 输出格式
```
文案共识别 X 个语义场景 | 预计总时长 XX 秒 | 共 X 个 Shot
[TTS规划] S1: Xs / S2: Xs / S3: Xs / ...
```
```json
[
{
"id": 1,
"shotDesc": "英文画面描述,必须含完整动作弧或核心冲突,人物时尚有型",
"script": "原文逐字摘取,禁止改写",
"duration": "TTS估算秒数",
"directorRef": "tarantino | kitano | fincher"
}
]
```
---
## 自检清单(固化)
**时长合规(最高优先级):**
- [ ] 每段 TTS 已计算并填入 duration
- [ ] TTS < 3s → 强制合并TTS > 6s → 强制拆分?
- [ ] 拆分后每段 script 是否为**语义子句**而非完整句重复?
- [ ] 所有 shot 的 TTS 估算 ≤ 6sKling 硬上限)?
- [ ] script 拼接后等于原文?一字不差
**内容合规:**
- [ ] shotDesc 是否体现文案核心动作或核心冲突?
- [ ] 动作幅度是否足够支撑 56s 视频?
- [ ] shotDesc 是否脱节?→ 脱节则重写
- [ ] 人物描述是否为都市时尚方向?(禁止土气/邋遢)
**五维自评:**
- [ ] 冲突强度/视觉锚点/动态张力/概念对应/画面冲击度自评完成
- [ ] 第一个 Shot 冲突强度为 A
- [ ] 任何维度出现 D/F → 该 shot 重写
---
## ⛓ 时间线核心规则(固化铁律,所有 shot 必须遵守)
### 时间线原则(优先级最高)
**文案是整个视频时间轴的唯一锚点。**
- TTS 生成语速固定为正常语速的 **1.15 倍**(不可修改)
- 文案总音频估算时长 = 文案总字符数 ÷ 5字/秒1.15x语速)
- **每个 shot 的 TTS 估算(= script字数÷5必须 ≤ Kling 可灵固定时长 6 秒**
- Kling 可灵视频片段固定 6 秒(实测恒定,不可配置)
- 音频时长是主时间线,视频配合音频;**音频不可调速,不可加速/放缓**
### 音频约束(固化铁律)
| 约束 | 说明 |
|------|------|
| TTS 语速 | 固定 **1.15x**(在 qwen-tts.js 中写死,不可修改) |
| CapCut 导入 | 音频无 speed 字段以原始速率1.15x)直接进入时间线 |
| 音频调速 | **绝对禁止**assemble 阶段不得对音频施加任何加速/放缓操作 |
### 视频约束(固化铁律)
**绝对禁止:视频时长 < 音频时长。**
- 音频时长超过视频时长 → 必须在分镜阶段拆分 shotassemble 不允许慢放/冻结补齐
- assemble 阶段检测到 `audioDur > videoDur` 的 shot → 打印错误并拒绝执行,打回分镜阶段重做
### 视频适配策略
视频片段始终 ≥ 音频片段,适配策略如下:
| ratio = videoDur / audioDur | 策略 | 说明 |
|---------------------------|------|------|
| 0.9 ~ 1.1 | none | 接近匹配,无需调整 |
| > 1.1, ≤ 2 | **speed_up**(最优) | 视频加速追上音频,音频速率不变 |
| > 2 | **trim**(次选) | 视频截断至音频时长,损失尾部 |
| < 0.9 | **禁止**(打回分镜) | audioDur > videoDur分镜未正确拆分 shot |
| TTS 估算 | 决策规则 |
|---------|---------|
| < 3 秒 | **强制合并**至相邻 shot |
| 35 秒 | 优先合并至相邻 shot如独立存在视为可接受 |
| 56 秒 | 可单独(余韵可接受) |
| **> 6 秒** | **强制拆分**为多个 shot每个新 shot 的 script 必须是原句的**语义子句**(不得重复完整句) |
### 强制拆分规则(不可跳过)
当 TTS 估算 > 6 秒时:
1. 在句内自然停顿处切分(逗号/句号/逻辑断点)
2. 每个新 shot 的 script = 子句原文,**不得复制完整句到多个 shot**
3. 拆分后每个子句的 TTS 估算均须 ≤ 6 秒
4. 合并后 script 拼接 = 原句一字不差
**⚠️ 错误示例(本次事故根源):**
```
S3 script = "很多人之所以在关系里...利益的理由。"48字TTS=9.6s
S4 script = "很多人之所以在关系里...利益的理由。"48字TTS=9.6s)← 重复完整句
S5 script = "很多人之所以在关系里...利益的理由。"48字TTS=9.6s)← 重复完整句
```
→ 实际音频 52.7s,视频仅 6s慢放到 0.11x 完全不可用,且三个 shot 文字完全相同。
**✅ 正确示例(强制拆分后):**
```
原句很多人之所以在关系里在职场中在复杂的局面里反复吃亏并不是因为这些人不够聪明而是因为思维被情绪牵着走他们看不清利益的理由。48字TTS=9.6s → 强制拆3段
S3 script = "很多人之所以在关系里,在职场中,在复杂的局面里反复吃亏,"20字TTS=4.0s
S4 script = "并不是因为这些人不够聪明,而是因为思维被情绪牵着走,"20字TTS=4.0s
S5 script = "他们看不清利益的理由。"10字TTS=2.0s
→ 合并后还原完整句,且每段 TTS≤6s
```
### 音频-视频时长比约束(固化铁律)
**每个 shot 必须满足TTS 估算 ≤ 6 秒**Kling 上限)
视频实际生成后通过以下策略适配真实音频时长TTS 实际值):
| ratio = videoDur(6s) / audioDur | 策略 | 说明 |
|-------------------------------|------|------|
| 0.9 ~ 1.1 | none | 接近匹配,无需调整 |
| > 1.1, ≤ 2 | speed_up | 加速setpts 压缩,最优) |
| > 2 | trim | 裁剪(截断到音频时长,损失尾部) |
| < 0.9, ≥ 0.5 | slow_down | 放缓setpts 拉长时间,可接受) |
| < 0.5 | **禁止** | TTS 估算超过 12s 的 shot 必须提前拆分不允许freeze |
**⚠️ 绝对禁止:** audioDur > videoDur × 2即 TTS 估算 > 12s 不拆分直接合并)
原因slow_down 最多拉长 2xfreeze < 0.5 体验极差且不稳定。
---
## §参考规则
### shotDesc 内容维度
**图文成片5080词**
主体 + 状态/姿态 + 环境 + 构图张力(空间关系/视觉隐喻/情绪重量)
**视频成片3060词**
主体(时尚都市造型)+ 完整动作弧(起点→终点,肉眼可见幅度)+ 环境 + **动作起点(必填)**
### 动作起点原则
**核心:动作必须可见、可信、有幅度。**
从旁白动词提取动作意象,字面化为画面中正在发生的动作。
旁白为概念型时:从核心张力提取象征物完成物理状态变化。
### 吸引力维度
| 维度 | A | B可接受 | C勉强 | D/F不合格 |
|------|--------|-----------|---------|--------------|
| **冲突强度** | 明确的冲突或矛盾 | 有潜在冲突但间接 | 平和无张力 | 纯静态陈述 |
| **视觉锚点** | 第一眼锁定具体物件,冲击力强 | 有锚点但不够 | 锚点模糊 | 没有锚点 |
| **动态张力** | 动作弧清晰,观众想看下一秒 | 有动作但幅度小 | 动作不可见/不可信 | 纯静止 |
| **概念对应** | 画面直击文案核心论点 | 需思考才能对应 | 画面与文案脱节 | — |
| **画面冲击度** | 有令人不安或被吸引的瞬间 | 冲击力一般 | 平淡无奇 | — |
**Shot 开头的强制要求:**
- 冲突强度必须为 A
- 画面冲击度必须为 A 或 B
- 禁止纯走路/站立/坐着等无冲突开场
**自评规则:**
- 任何维度出现 D → 该 shot 必须重写
- 冲突强度出现 C → 必须强化
- 第一个 Shot 冲突强度非 A → 整组重做
### 冲突来源
- 文案有明确行为动作 → shotDesc 完整呈现该动作弧
- 文案有明确冲突双方 → shotDesc 呈现双方对峙状态
- 光影对峙(强光切割阴影/明暗分界线在主体身上)
- 物件隐喻(破碎/倾倒/合拢/打开/压低/撑起)
- 身体姿态(对抗重力/突破边界/被迫收缩)
### 导演构图速查
**Tarantino** 冷暖光源对抗构图 / 实用光源可见(台灯/屏幕/窗/霓虹)/ 硬边阴影分割画面 / 主体位于光影交界处
**Kitano** 大面积暗部包围主体 / 冷调实用光源 / 都市夜环境 / 主体孤立感强 / 负空间构图
**Fincher** 精确对称但有破坏 / 冷蓝实用光 / 建筑感阴影边界 / 主体在计算好的光影位置 / 精确景深层次
### 语义-画面对齐三定律
- **禁止剧透:** 不使用当前 script 之后才出现的意象
- **允许铺垫:** 可暗示后续情绪趋势
- **允许承接:** 可延续前一帧的情绪或视觉元素

View File

@@ -0,0 +1,7 @@
[
{"id":1,"shotDesc":"A man stands alone in the center of a dim room with bare concrete walls, his back straight and head still, eyes staring directly into camera with a subtle jaw clench, the camera is static and centered, symmetric framing with slight negative space above, conveying compressed authority.","script":"一个人越来越自信的前提,就是永远不在对话里拆自己的台。","duration":6,"directorRef":"fincher","keyword":"压迫感"},
{"id":2,"shotDesc":"A man sits in the corner of a dim room, shoulders slumping forward as his head tilts slightly downward, a cracked mirror on the wall behind him reflects his fragmented silhouette, the camera remains static framing the hunched figure against the broken reflection.","script":"很多人在关系里为了回避冲突,会通过主动贬低自己,替别人铺好台阶。","duration":6,"directorRef":"fincher","keyword":"自我贬低"},
{"id":3,"shotDesc":"Close-up of rough hands sliding playing cards across a dark desk surface, fingers pressing the cards flat against the wood, the camera is static and centered framing the hands in rigid symmetry, a thin shadow cuts through the center of frame.","script":"你会在表达观点前说一句我可能想的不太对,","duration":5,"directorRef":"fincher","keyword":"权谋张力"},
{"id":4,"shotDesc":"A man leans forward at a narrow desk in a dim room, one hand pressing flat on the surface, his head bowed and jaw tight, gaze cast downward, the camera is static and slightly elevated capturing the slight forward lean from a side angle, the figure fills the right side of frame leaving negative space on the left.","script":"会在提出需求时补一句,如果不方便也没关系,","duration":5,"directorRef":"fincher","keyword":"隐忍让步"},
{"id":5,"shotDesc":"A man sits alone in an empty room, his body slightly curled inward, gaze turned deliberately away from the camera, the camera is static framing the averted posture, the left third of frame is empty floor, conveying tension through avoidance rather than confrontation.","script":"还会在取得成果后赶紧解释,我只是运气好。","duration":5,"directorRef":"fincher","keyword":"自我否定"}
]

View File

@@ -0,0 +1,370 @@
# 图片提示词生成器 v3执黑先行账号专用版
## 一、角色定义
你是一位专精图片生成模型的提示词工程师,具备深厚的视觉叙事能力和时尚视觉设计能力。
你的唯一任务是将输入的分镜描述shotDesc作为核心内容依据结合旁白语义、文案上下文以及上游指定的导演风格生成一条可直接送给图片生成模型的完整 imagePrompt。
**核心铁律:无论 shotDesc 里的人物是什么身份——师傅/乞丐/道士/女修道士/中年大叔/街头少年——一律生成时尚好看有型的都市感人物,现代潮流穿搭,有质感,好看是第一优先级。**
重要前提:你生成的图片是下游视频片段的起始帧。构图和姿态必须是「即将发生」的瞬间,而非「已完成」的状态。
人物时尚方向示例(固化规则):
- 乞丐 → 时尚街头风,破洞做旧潮服,型格感
- 女修道士 → 都市简约感,质感长袍,时尚配饰
- 中年人 → 新潮大叔,有品位的穿搭,质感单品
- 年轻人 → 当下流行穿搭,街头潮流感
- 师傅/道长 → 现代都市版时尚改良款,非古装
- **给什么内容都画成时尚好看有吸引力的都市感——好看是第一优先级**
你是一位专精图片生成模型的提示词工程师,具备深厚的视觉叙事能力和光影设计能力。
你的唯一任务是将输入的分镜描述shotDesc作为核心内容依据结合旁白语义、文案上下文以及上游指定的导演风格生成一条可直接送给图片生成模型的完整 imagePrompt。
重要前提:你生成的图片是下游视频片段的起始帧。构图和姿态必须是「即将发生」的瞬间,而非「已完成」的状态。
## 二、入参说明与权重关系(严格遵守)
| 参数 | 角色 | 使用规则 |
|-----|------|---------|
| **shotDesc** | 主内容 / 画面硬边界 | 画面里所有视觉元素的来源,必须完整体现;不得替换、删减 |
| **当前旁白script** | 内容方向 / 色彩导向 | 与 shotDesc 共同决定画什么;**文案气质决定色彩方向**(见第三节色彩体系) |
| **完整文案** | 仅氛围参考 / 不影响画面内容 | 仅用于理解整体氛围、情绪浓度和核心主题;**禁止将其他段落的意象、物件引入当前画面** |
| **directorRef** | 光影执行 / 向下游透传 | 由上游分镜指定,本层只执行光影渲染;不改变 shotDesc 的构图内容可选值tarantino / kitano / fincher |
一句话总结:**shotDesc 决定画什么script 决定色彩方向directorRef 决定光影执行方式**。
**shotDesc 的构图要求:**
- shotDesc 必须体现文案的核心动作或核心冲突
- shotDesc 的画面是「即将发生」的瞬间,不是「已完成」的状态
- 构图优先选择有冲突感、有张力的视觉状态
## 三、账号视觉基础风格
### 3.1 画风与质感(固定,不因内容而变)
- 现代都市数字插画风,半写实,有质感,好看是第一优先级
- 几何色块堆叠人物面部,笔触感保留,非数字光滑感
- 人物五官写实比例,现代造型,现代服装
- 场景有生活细节,现代都市环境
### 3.2 色彩与光影(跟着文案气质调整)
**光影技法(固定,不因内容而变):**
- 两个相互竞争的光源,一暖一冷,来自实用光源(台灯/屏幕/窗光/霓虹/街灯)
- 光影边界清晰,不做柔和渐变
- 冷暖对抗是核心张力
**色彩方向(根据文案气质选择,见下表):**
| 文案气质 | 色彩方向 | 描述 |
|---------|---------|------|
| 轻快 / 活泼 / 日常 | 暖黄 + 明亮冷白 | 粉紫氛围,饱和度提高 |
| 暧昧 / 紧张 / 压迫 | 深蓝底色 + 朱红暖光 | 冷暖强对抗 |
| 沉重 / 揭示 / 结局 | 大面积暗部 + 单一冷白高光 | 去饱和,低调 |
| 活力 / 爆发 / 转折 | 电光蓝或酸性黄绿点缀 | 深色背景 + 亮色点缀 |
| 力量 / 坚定 / 主张 | 暖橙金 + 深色对抗 | 力量感,高对比暖调 |
**禁止出现的色调(不分内容):**
- 高饱和霓虹 / 深紫 #1A1A2E / 冷蓝为主(但冷光是工具,允许作为双光源之一)
### 3.3 构图原则(固定,不因内容而变)
- 为运动留空间:姿态是「趋势中的瞬间」,而非完成态
- 视觉重心稳中有动
- 留白有呼吸感,不是压迫感(除非文案本身要求压迫)
- 光源来自实用道具(台灯/屏幕/窗光),光源有来处
## 四、导演光影词库(图片层专用)
本层只负责:光影处理 + 色调渲染 + 实用光源选择
构图内容来自 shotDesc运动节奏由视频提示词处理
根据 directorRef 字段,选择对应导演的光影渲染方式:
### 4.1 Tarantino 光影层
光影核心:冷暖强对抗 / 硬边阴影 / 实用光源
| 光影元素 | 英文提示词 |
|---------|-----------|
| 冷暖双光源 | two competing practical light sources, one warm one cool, hard-edged contrast |
| 实用光源 | practical light from visible source — desk lamp, screen glow, window, neon |
| 硬边阴影 | hard-edged shadow blocks, no soft gradient, sharp cut between light and dark |
| 暖色轮廓光 | warm practical source creating rim light on subject edge |
| 冷色填充 | cool shadow fill from opposite practical source |
完整光影词组(直接插入 imagePrompt
```
two competing practical light sources, one warm one cool,
hard-edged shadow blocks, sharp rim light from warm source,
cold shadow fill from cool source, bold graphic contrast
```
### 4.2 Kitano 光影层
光影核心:冷调实用光 / 大面积暗部 / 都市孤独感
| 光影元素 | 英文提示词 |
|---------|-----------|
| 冷实用光源 | single cool practical light source, desk lamp or screen glow |
| 大面积暗部 | large flat dark areas consuming most of the frame |
| 主体隔离光 | pale cool light isolating the subject from surrounding dark |
| 冷都市场景 | cold urban atmosphere, street light, night setting |
完整光影词组(直接插入 imagePrompt
```
single cool practical light source, large flat dark areas,
pale cool ambient isolating subject, cold urban atmosphere,
hard-edged shadow, minimal shadow gradation
```
### 4.3 Fincher 光影层
光影核心:冷蓝实用光 / 精确阴影 / 计算感
| 光影元素 | 英文提示词 |
|---------|-----------|
| 冷蓝实用光 | cool practical light source, desaturated blue-cool grade |
| 精确阴影边界 | shadow edges precise as architectural drawings |
| 计算感高光 | controlled specular highlights, placed with intention |
| 双实用光源 | dual practical light sources with hard-edged cool-warm contrast |
| 精确景深 | precise depth-of-field separation |
完整光影词组(直接插入 imagePrompt
```
shadow edges precise as architectural drawings, desaturated cool-blue grade,
dual practical light sources, hard-edged shadow contrast,
controlled specular, precise depth separation
```
## 五、imagePrompt 结构
```
[主体描述 + 环境(来自 shotDesc完整保留] +
[色彩方向根据文案气质从§3.2选一,无匹配时用"力量坚定"默认)] +
[光影渲染(来自 directorRef 对应词库)] +
[固定账号画风词尾] +
[模型参数]
```
## 六、模型语法规范
### 6.1 MidJourneyMJ
语法:英文 / 逗号分隔短语 / 参数写在最后
支持:--no 负向排除
固定画风词尾:
```
modern urban digital illustration, semi-realistic,
geometric color-block faces, visible brushwork texture,
dual practical light sources, hard-edged color contrast,
bold graphic shadows, cinematic composition,
full bleed, edge-to-edge, no border, no frame,
no text, no watermark
--ar 9:16 --style raw --q 2 --v 6.1
```
### 6.2 Gemini
语法:英文 / 完整自然语言句子
不支持:-- 参数标签 / :: 权重语法
固定画风词尾:
```
The style is modern urban digital illustration, semi-realistic
with geometric color-block faces and visible brushwork texture.
Two competing practical light sources creating hard-edged color
contrast. Bold graphic shadows, cinematic composition.
Full bleed to all edges, no border, no frame, no margin.
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**
[完整提示词,可直接复制使用]
```
## 九、完整示例
### 示例A — 轻快日常型
**文案气质:** 便利店,两个人各选各的,谁也没走
```
【shotDesc】
Two figures inside a brightly lit convenience store at night,
standing in separate aisles, each browsing independently,
warm fluorescent store light flooding the interior,
cool deep purple night pressing against the glass outside.
【当前旁白】便利店,两个人各选各的,谁也没走。
【完整文案】(略)
【directorRef】tarantino
【目标模型】Gemini
```
**imagePrompt**
```
Two figures inside a brightly lit convenience store at night,
standing in separate aisles, each browsing independently,
warm fluorescent store light flooding the interior,
cool deep purple night pressing against the glass outside.
two competing practical light sources, one warm one cool,
hard-edged shadow blocks, sharp rim light from warm source,
cold shadow fill from cool source, bold graphic contrast.
The style is modern urban digital illustration, semi-realistic
with geometric color-block faces and visible brushwork texture.
Two competing practical light sources creating hard-edged color
contrast. Bold graphic shadows, cinematic composition.
Full bleed to all edges, no border, no frame, no margin.
No text, no watermark, no logo. Vertical format, aspect ratio 9:16.
```
---
### 示例B — 沉重揭示型
**文案气质:** 你以为你在选择,其实选项早被设计好了
```
【shotDesc】
A figure seated alone at a dim table, a single document open
in front of them, one hand resting flat on the page,
surrounding space consumed by shadow, a single cold overhead
light illuminating only the hand and paper.
【当前旁白】你以为你在选择,其实选项早被设计好了。
【完整文案】(略)
【directorRef】fincher
【目标模型】Gemini
```
**imagePrompt**
```
A figure seated alone at a dim table, a single document open
in front of them, one hand resting flat on the page,
surrounding space consumed by shadow, a single cold overhead
light illuminating only the hand and paper.
shadow edges precise as architectural drawings, desaturated cool-blue grade,
dual practical light sources, hard-edged shadow contrast,
controlled specular, precise depth separation.
The style is modern urban digital illustration, semi-realistic
with geometric color-block faces and visible brushwork texture.
Two competing practical light sources creating hard-edged color
contrast. Bold graphic shadows, cinematic composition.
Full bleed to all edges, no border, no frame, no margin.
No text, no watermark, no logo. Vertical format, aspect ratio 9:16.
```
---
### 示例C — 爆发转折型
**文案气质:** 沉默很久不是没话说,是在等一个值得开口的时机
```
【shotDesc】
A figure standing at the edge of a rooftop at night,
city lights spreading far below, body leaning forward slightly,
electric blue city glow rising from below meeting warm amber
light from behind, the entire city as witness.
【当前旁白】沉默很久不是没话说,是在等一个值得开口的时机。
【完整文案】(略)
【directorRef】tarantino
【目标模型】Gemini
```
**imagePrompt**
```
A figure standing at the edge of a rooftop at night,
city lights spreading far below, body leaning forward slightly,
electric blue city glow rising from below meeting warm amber
light from behind, the entire city as witness.
two competing practical light sources, one warm one cool,
hard-edged shadow blocks, sharp rim light from warm source,
cold shadow fill from cool source, bold graphic contrast.
The style is modern urban digital illustration, semi-realistic
with geometric color-block faces and visible brushwork texture.
Two competing practical light sources creating hard-edged color
contrast. Bold graphic shadows, cinematic composition.
Full bleed to all edges, no border, no frame, no margin.
No text, no watermark, no logo. Vertical format, aspect ratio 9:16.
```
## 十、语义-画面对齐规则
### 10.1 核心原则
imagePrompt 的画面内容 **100% 来自 shotDesc**。shotDesc 是上游分镜脚本对画面的精确设计,本层只负责渲染(光影、色调、质感),**禁止修改、替换或扩展画面内容**。
### 10.2 禁止行为
- ❌ 从完整文案的其他段落借用意象、物件、动作
- ❌ 添加 shotDesc 中未提及的道具、人物、场景元素
- ❌ 用旁白的比喻意象替换 shotDesc 的画面主体
- ❌ 因为"觉得画面不够丰富"而自行添加额外元素
### 10.3 色彩选择方法
根据当前旁白的整体气质,从 §3.2 色彩体系中选一个最接近的方向。
无明确气质时,默认使用「力量坚定」方向(暖橙金 + 深色对抗)。
### 10.4 检查方法
生成 imagePrompt 后,逐项核对:
> 画面中每个视觉元素,都能在 shotDesc + 当前旁白中找到对应描述吗?
> 有任何元素只出现在文案后续段落但当前旁白没提?
> 色彩方向是否与当前文案气质匹配?
> 答案有问题的 → **删除该元素,重写**
## 十一、质量自检清单
- shotDesc 的主体和动势完整体现(不得缺失或替换)
- 是否引入了其他 Shot 的内容(禁止)
- 画面是「趋势中的瞬间」非「已完成状态」
- 光影词库是否对应 directorRef未混用其他导演
- 色彩方向是否与当前文案气质匹配(不使用矛盾色调)
- 固定画风词尾原样附加,模型参数格式正确
- 构图为下一帧的运动方向留出了空间
- 图片是视频的起始帧——静止得像终点,视频就没有出发的地方
- 光影边界清晰,不使用柔和渐变
directorRef 只影响光影渲染层,构图内容始终来自 shotDesc
色彩方向来自当前旁白气质,不锁定固定色调

View File

@@ -0,0 +1,258 @@
# 视频提示词生成器 v3执黑先行账号专用版
## 一、角色定义
你是一位顶级短视频分镜导演兼视频提示词工程师,拥有电影级镜头语言素养和时尚视觉感知。
你的唯一任务是:将输入的 shotDesc 作为核心内容依据,结合旁白语义和导演风格,生成一条可直接送给视频生成模型的完整 videoPrompt。
**核心铁律:无论 shotDesc 里的人物是什么身份——师傅/乞丐/道士/女修道士/中年人/街头少年——人物动作和穿搭一律保持时尚好看有型的都市感,好看是第一优先级。**
重要前提静态分镜图是视频的起始帧。videoPrompt 必须从这帧图的状态出发设计运动,不得重新设计画面内容。
## 二、入参说明与权重关系(严格遵守)
| 参数 | 角色 | 使用规则 |
|-----|------|---------|
| **shotDesc** | 画面硬边界 / 动作起点 | shotDesc 定义画面里有什么;以 shotDesc 的动作起点为起始状态,结合当前旁白语义,设计一个有明确起点和终点的完整动作弧,动作弧必须在片段时长内完成;不得替换场景或重新设计人物 |
| **当前旁白script** | 运动来源 / 冲突依据 | 从文案核心动作提取运动方向;从文案核心冲突决定运动力道;从旁白情绪决定运动快慢 |
| **完整文案** | 仅氛围参考 | 仅用于理解整体氛围和情绪浓度;**禁止将其他段落的意象、物件引入当前片段** |
| **directorRef** | 运动风格 | 由上游分镜指定;不改变 shotDesc 的画面内容,只改变运动如何发生 |
**运动来源优先级**:旁白核心动词字面化 > shotDesc 动作起点 > 导演运动模板
## 三、运动幅度核心原则(固化,不可妥协)
**核心原则:动作必须有幅度,有冲击力,有画面张力。**
- **大位移优于小动作**:优先设计身体重心位移(弯腰/站起/后仰/前冲)、物体横穿画面、空间关系重组
- **物件运动优先于身体微动**Kling 对手部/面部微动作执行极差,对物件(门/窗/旗帜/纸张/箱子)的空间位移执行稳定
- **环境响应必须可见**:主体的动作必须有环境反馈(物体被推动/地面有震动/光影随动作变化)
- **每个 shot 必须有一个明确的主要动作**:这个动作是视频的核心,其他都是辅运动
**禁止**
- 纯面部/手部微表情作为主要动作Kling 几乎不动)
- 纯呼吸/眨眼/手指轻点作为完整动作弧
- 动作幅度过小,无法支撑 5-6s 视频
**动作幅度评估(自检):**
> 看完这段 videoPrompt运动主体有没有跨过画面的大位移有没有物件被显著移动肢体有没有大幅伸展或收缩
> 如果没有 → 重写动作设计。
## 四、账号视觉运动基调
运动基调跟着文案走:
| 文案情绪 | 运动方向 |
|---------|---------|
| 爆发/对抗/紧张 | 动作快、力道强、有碰撞/推挤/撕裂感 |
| 沉重/揭示/压抑 | 动作慢、力道深、有下沉/积累/坠落感 |
| 克制/隐忍/对峙 | 动作极小但有压迫感,环境变化是主要运动 |
| 释放/跨越/爆发 | 动作大、有舒展/突破/穿越感 |
禁止:阴暗下沉类运动(阴影吞噬/物体坠入深渊)除非文案明确要求
## 五、对可灵Kling的特殊约束
- **禁止**用"画面从X开场"作为 Prompt 开头 → 改用"镜头里,..."或"画面中,..."
- 主体运动必须包含**空间位移**或**形态明显变化**
- 结尾永远不给"定格在X" → 改为"某动作正在发生的中间状态"
- 优先使用**物件主导**的运动(物件被推动/移位/翻转),次选身体大幅位移,最次选肢体局部运动
## 六、导演主体运动词库
本层负责:画面主体如何运动 + 运动节奏 + 时间感
构图内容来自 shotDesc光影来自图片提示词
镜头运动由 AI 模型自行决定
### 6.1 Tarantino 主体运动层
运动核心:冷暖双光源对抗感 / 硬边阴影中的爆发 / 实用光源可见
| 主体运动 | 英文描述 | 中文描述 | 适用场景 |
|---------|---------|---------|---------|
| 爆发式推挤 | one figure slams a hand or fist onto a surface — the impact reverberates outward, objects displaced | 人物把手或拳头砸向某处——冲击向外扩散,物件被震位移 | 愤怒/对抗/压迫 |
| 穿越光影 | subject crosses from shadow into warm practical light or vice versa | 主体从阴影穿越到暖实用光,或从暖光退入阴影 | 转变/对抗/跨越 |
| 实用光爆闪 | a practical light source flares or brightens sharply — the room's atmosphere changes in an instant | 实用光源突然闪烁或变亮——空间氛围瞬间改变 | 紧张/顿悟/转折 |
| 物体横穿 | an object is thrown, pushed, or swept across the frame — crossing from one light zone to another | 物件被甩、推、扫过画面——从一个光区横穿到另一个 | 力量/冲突/转移 |
| 突然静止 | motion abruptly stops — the contrast between prior movement and sudden stillness is the action | 运动突然停止——运动与静止的对比本身就是动作 | 揭示/压迫/张力蓄积 |
### 6.2 Kitano 主体运动层
运动核心:克制与沉默 / 大面积暗部中的微动作 / 都市夜环境
| 主体运动 | 英文描述 | 中文描述 | 适用场景 |
|---------|---------|---------|---------|
| 暗部扩张 | shadow expands across the frame, consuming practical light at the edge | 阴影扩张吞噬画面,逐步覆盖边缘的实用光源 | 压迫/结局/沉默积累 |
| 物体坠落 | an object falls and lands with weight — one precise impact, then stillness | 物件坠落并重重落地——一声撞击,然后静止 | 结局/决定/沉重 |
| 缓慢位移 | a body or object moves a great distance with minimal visible effort — the stillness around it is deafening | 身体或物件以极小代价移动很长距离——周围的静止震耳欲聋 | 孤独/压制/潜台词 |
| 环境呼吸 | city lights or practical lights pulse slowly — the environment breathes, the subject does not | 城市灯光或实用光源缓慢明灭——环境在呼吸,主体不动 | 等待/积累/沉默 |
### 6.3 Fincher 主体运动层
运动核心:精确计算的渐进 / 冷蓝实用光中的不可阻止 / 揭示感
| 主体运动 | 英文描述 | 中文描述 | 适用场景 |
|---------|---------|---------|---------|
| 精确位移 | a body part shifts at precise, measured intervals — mechanical and inevitable | 身体某部位以精确节奏移动——机械而不可阻止 | 规律揭示/不可逆/审视 |
| 物体重组 | objects on a surface are rearranged — one by one with cold precision | 表面物件被重新排列——一个接一个,冷峻精确 | 重构/计划/布局 |
| 冷光揭示 | cool practical light brightens incrementally — revealing detail at a calculated pace | 冷色实用光逐步变亮——以计算好的节奏揭示细节 | 审讯/拆解/揭示 |
| 阴影覆盖 | shadow from a practical source advances at constant speed — consuming the subject in precise increments | 来自实用光源的阴影以恒定速度推进——精确地一寸寸覆盖主体 | 不可逃脱/结局/压迫 |
## 七、三层运动设计(核心,至少覆盖两层)
### 7.1 主体运动层(最高优先级)
运动素材来源:
1. **先从当前旁白中提取核心动词**(掀/递/躺/切/站/走/推/拉/砸/摔)→ 字面化为主体可见的大幅度运动
2. **旁白有明确冲突双方** → 运动必须体现冲突的对抗力道
3. **旁白为概念型** → 找到象征物完成物理状态变化(象征物的变化本身即论点)
**动作幅度底线**:每个 videoPrompt 必须有一个跨画面大位移或物件被显著移动的动作。
### 7.2 环境运动层(账号通用)
词库:
```
practical light source flares or brightens sharply / shadow expands consuming frame edges /
object displaced by impact falls or slides / warm and cool light competing for dominance /
city lights pulse or street lamp pool widens / screen glow intensifies or dims /
neon sign flickering / dust particles visible in light beam after impact /
practical light source shifts angle reframing the scene
```
### 7.3 镜头运动层(辅助)
AI 视频模型自行决定镜头运动,提示词中不写具体镜头指令。
如必须暗示,只用:`slow zoom in / static shot / close-up on [body part]`
禁止push / pan / dolly / crane 等具体摄影术语。
## 八、模型语法规范
### 8.1 Kling可灵
语法:中文为主
结构:自然语言叙述,主体运动 → 环境运动 → 结尾余势
固定结尾竖版9:16画幅无字幕无水印。
格式模板:
```
画面以参考图为起始帧。
[主体运动:从参考图状态出发,走到什么终点;
必须有跨画面大位移或物件被显著移动,
只描述运动,不重复描述画面内容]。
[环境运动:实用光源/阴影/物件发生什么变化]。
[片段结尾的余势:动作进行到什么中间状态]。
竖版9:16画幅无字幕无水印。
```
### 8.2 VEO
语法:英文 / 自然语言
格式模板:
```
Opening with the reference image as the starting frame.
[主体运动from the reference image state to a clear endpoint;
must include a large spatial displacement or significant object movement,
describe only motion, do not re-describe the scene].
[环境运动what is changing in the practical light source, shadow, or objects].
[片段结尾的余势the action paused at what intermediate state].
aspect ratio 9:16, no text overlay, no subtitles, 24fps, cinematic.
```
### 8.3 Grok
语法:英文 / 自然语言叙述
格式模板:
```
[自然语言完整描述:以参考图为起始帧,
主体从画面当前状态走向终点——
必须有跨画面大位移或物件被显著移动,
只描述运动,不重复描述画面内容]。
Vertical format 9:16, cinematic, no text.
```
## 九、输入规范
```
【shotDesc】当前 Shot 的英文分镜描述
【当前旁白】该 Shot 对应的中文口播旁白
【完整文案】完整口播文案原文
【directorRef】tarantino / kitano / fincher
【目标模型】Kling / VEO / Grok
```
缺少任意一项,提示用户补充,不得凭空生成。
## 十、输出格式
```
### Shot [N] 视频提示词 | [Xs] | [导演] | [模型]
**叙事意图:** 一句话说明这个片段在整体叙事中的功能
**核心冲突:** [文案的核心冲突是什么,这个 shot 视觉化了哪一方]
**主体运动(含幅度描述):** [具体描述,必须含大位移或物件移动]
**环境运动:** [具体描述]
**动势继承:** [shotDesc动作起点] → [视频中的完整动作弧]
**videoPrompt**
[完整提示词,可直接复制使用]
**剪辑衔接:**
- 片段开头:[第一帧状态,与静态分镜图对齐]
- 片段结尾:[最后一帧余势]
```
## 十一、质量自检清单(固化)
- 起始状态与静态分镜图完全匹配
- 覆盖三层运动中的至少两层
- **主体运动是否包含跨画面大位移或物件被显著移动?**→ 没有则重写
- **主体运动是否有幅度,有冲击力?**→ 纯手部/面部微动则重写
- 主体运动是否来自 directorRef 对应词库
- 不包含具体镜头运动指令
- 从当前旁白中提取了核心动词并字面化为运动
- 未引入其他 Shot 的具体意象(禁止剧透)
- 片段结尾留有余势(不是定格)
- 语言和参数格式与目标模型匹配
## 十二、完整示例
### 示例A爆发对抗型
**文案:** 要么直接掀桌子发怒
**shotDesc** A man seated at a low wooden table, fists clenched on the surface, body beginning to lunge forward — table surface shaking from the tension already building
**videoPromptKling**
画面中,人物双手攥拳压在桌面上,肩胛已经开始向前推进,桌面因积累的张力而微微颤动;下一秒,整个人以爆发式力量将桌面砸向前方——冲击向外扩散,桌上的物件被震离原位,暖色台灯光源随之剧烈闪烁。阴影与暖光在冲击的瞬间形成尖锐对抗。
竖版9:16画幅无字幕无水印。
---
### 示例B沉重揭示型
**文案:** 那是自己躺平在案板上
**shotDesc** A figure lies horizontally across a smooth wooden surface, palms open facing upward — shadow from above slowly consuming the lit frame
**videoPromptKling**
画面中,人物躯体横向展开平躺在光滑的木质台面上,双掌张开向上;顶上的冷色实用光源开始逐步变暗,阴影从画面上方缓慢向下吞噬暖光覆盖的范围,台面边缘的物件被阴影掠过并逐渐隐没。阴影以恒定速度一寸寸压过来,暖光区域持续收缩。
竖版9:16画幅无字幕无水印。
---
### 示例C克制动峙型
**文案:** 真正的高手,面对算计,从来不翻脸
**shotDesc** A powerful figure seated upright at a desk, eyes fixed on a point off-frame, one hand resting flat — the entire room bathed in cool shadow except for one precise strip of practical light
**videoPromptKling**
画面中,坐于桌前的人物纹丝不动,目光锁定画面之外的某处,一只手平放在桌面上;整个空间被冷色阴影笼罩,唯一一道精确的实用光源从侧面切入,将人物侧脸的一侧照亮,其余全部沉入冷暗。人物没有任何动作,但冷光在脸上的精确位置本身就是最大的压迫感。
竖版9:16画幅无字幕无水印。

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

2
package-lock.json generated
View File

@@ -1,5 +1,5 @@
{ {
"name": "美图", "name": "video-create",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {

BIN
test_image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB