修复bug

This commit is contained in:
zhishi 2026-04-02 01:40:37 +08:00
parent 2b38e46d74
commit d8349ba3b9
8 changed files with 452 additions and 134 deletions

View File

@ -19,16 +19,16 @@
## 制作流水线
个阶段**必须按顺序执行**
个阶段**必须按顺序执行**
```
阶段1: 衍生资产分析 → 阶段2: 衍生资产生成(可选) → 阶段3: 导演规划 → 阶段4: 构建分镜表 → 阶段5: 生成分镜
阶段1: 衍生资产分析 → 阶段2: 衍生资产生成(可选) → 阶段3: 导演规划 → 阶段4: 构建分镜表 → 阶段5: 分镜面板写入 → 阶段6: 分镜图生成
```
### 全局约束
- **资产约束**阶段3、4、5 只能使用资产库中已存在的资产含阶段1已写入的衍生资产
- **异步操作**阶段2的图片生成、阶段5的分镜图片生成均为异步操作,派发后告知用户等待即可
- **资产约束**阶段3、4、5、6 只能使用资产库中已存在的资产含阶段1已写入的衍生资产
- **异步操作**阶段2的图片生成、阶段6的分镜图片生成均为异步操作,派发后告知用户等待即可
- **审核规则**仅阶段3导演规划和阶段4构建分镜表需要审核执行完毕后自动派发监督层
---
@ -72,7 +72,7 @@
| 前置条件 | 阶段1完成且用户确认生成 |
| 审核 | 不需要 |
**决策层行为:** 将用户确认的资产清单(或子集)派发给执行层。返回确认后,告知用户图片生成中,直接进入阶段3。
**决策层行为:** 将用户确认的资产清单(或子集)派发给执行层。返回确认后,告知用户图片生成中,询问用户是否进入阶段3。
---
@ -104,27 +104,40 @@
---
### 阶段5构建分镜面板
### 阶段5分镜面板写入
| 项 | 说明 |
|----|------|
| 派发 | 执行层调将分镜表拆分生成分镜面板 |
| 输出 | 生成的分镜面板 |
| 派发 | 执行层按分镜表写入分镜面板 XML |
| 输出 | 分镜面板写入完成确认 |
| 前置条件 | 阶段4完成且用户确认 |
| 审核 | 不需要 |
**决策层行为:**
向执行层派发阶段5分镜面板写入任务收到确认后进入阶段6。
### 阶段6生成分镜
**阶段特有约束:**
- 必须严格依据阶段4分镜表逐行写入行数与时长保持一致
- 分组累计时长不得超过 15 秒
---
### 阶段6分镜图生成
| 项 | 说明 |
|----|------|
| 派发 | 执行层调用图片生成接口生成分镜图片 |
| 输出 | 生成的分镜图片 |
| 前置条件 | 阶段5完成且用户确认 |
| 派发 | 执行层读取分镜面板并调用图片生成接口 |
| 输出 | 分镜图片生成任务启动(异步) |
| 前置条件 | 阶段5完成 |
| 审核 | 不需要 |
**阶段特有约束:** `generate_storyboard_images({ ids: [真实分镜ID列表] })` 中的 ids参数 必须指向分镜面板中实际存在的分镜Id。
**决策层行为:** 执行层返回确认后,告知用户图片生成已启动,流程结束。
**决策层行为:**
向执行层派发阶段6分镜图生成任务收到确认后告知用户任务已启动并结束流程。
**阶段特有约束:**
- 仅可使用分镜面板中的真实分镜 ID 发起生成
- 图片内容需与分镜描述一致
---
## 调度与派发规范

View File

@ -18,7 +18,8 @@
| 资产图片、生成资产、generate assets | [二、衍生资产图片生成](#二衍生资产图片生成) |
| 导演规划、拍摄计划、director plan | [三、导演规划](#三导演规划) |
| 构建分镜表、分镜面板、storyboard table | [四、构建分镜表](#四构建分镜表) |
| 生成分镜、分镜图片、storyboard gen | [五、生成分镜图片](#五生成分镜图片) |
| 分镜面板写入、写入分镜面板、storyboard panel | [五、分镜面板写入](#五分镜面板写入) |
| 生成分镜、分镜图片、storyboard gen | [六、分镜图生成](#六分镜图生成) |
---
@ -51,11 +52,11 @@
### `add_deriveAsset` 入参要求
```ts
add_deriveAsset({
assetsId: number, // 关联的资产ID
id: number | null, // 衍生资产ID新增填 null
name: string, // 衍生资产名称
desc: string, // 衍生资产描述
type: "role" | "tool" | "scene" | "clip", // 衍生资产类型
assetsId: number, // 关联的资产ID
id: number | null, // 衍生资产ID新增填 null
name: string, // 衍生资产名称
desc: string, // 衍生资产描述
type: "role" | "tool" | "scene" | "clip", // 衍生资产类型
})
```
@ -63,12 +64,12 @@ add_deriveAsset({
- `assetsId`:父资产在工作区中的 ID
- `id`:新增时必须为 `null`;更新已有衍生资产时填写已有衍生资产 ID
- `name`2~6 字,体现视觉外观变化
- `desc``[与默认态的差异] · [视觉特征] · [出现场景/触发条件]`1~100 字
- `desc``[与默认态的差异] · [视觉特征] 1~100 字
- `type`
- 角色资产填 `role`
- 道具资产填 `tool`
- 场景资产填 `scene`
- 镜头/片段类资产填 `clip`
- 角色资产填 `role`
- 道具资产填 `tool`
- 场景资产填 `scene`
- 镜头/片段类资产填 `clip`
@ -162,6 +163,7 @@ add_deriveAsset({
约束:
- 色调具体到色温范围或色彩倾向描述
- 光影以「段落-光影方向」表格呈现,每段落指定光影基调方向
- 色温、光源角度、冷暖色调分配等具体技法参数以风格技法参考(`director_planning`)为准
- **构图须说明叙事理由**,参考以下情绪-构图映射(按需选用):
- 对称构图 → 秩序 / 压迫 / 庄重
- 三分法偏侧留白 → 孤独 / 期待 / 未知
@ -208,6 +210,7 @@ add_deriveAsset({
约束:
- 配乐按段落统一规划(不逐场),同段落内场景切换靠环境音变化过渡
- 乐器选择、组合策略等具体技法以风格技法参考(`director_planning`)为准
- 环境音具体到可感知声源("蝉鸣 / 溪水 / 市井叫卖 / 雨滴檐角"),每场标注 1~2 个核心环境音
- 标注运用沉默手法的关键瞬间(关键情感瞬间优先考虑去掉配乐,只留环境音)
- 全片配乐覆盖率建议不超过 70%,留白段落与配乐段落形成呼吸感
@ -257,6 +260,26 @@ add_deriveAsset({
粒度:一个独立画面 = 一条分镜,约每 50~100 字剧本对应 1~2 条分镜。过渡/转场如有明确描写也单独拆分。
### 定场与镜头合并规则(防冗余)
**定场镜头**:每个新场景/段落的定场最多 1~2 个镜头完成,禁止拆成 3 个以上碎片。
- 推荐做法1 个带缓推的远景(定场+主体引入一镜完成),或 1 个大远景定场 + 1 个全景引入主体
- 禁止做法:先拍环境空镜→再拍局部细节→再拍人物到达的冗余三段式
**镜头合并自检**
- 能一镜交代的不拆两镜——如果一个带运镜的镜头能同时完成定场+引入,不要拆成两个
- 连续描述同一空间不同局部的镜头(院门→藤蔓→厢房)应合并为一个镜头,用画面描述涵盖多层空间
- 纯装饰性镜头(只展示环境细节无叙事推进)应合并到有叙事功能的镜头中
- **导演思维检验**:写完后自检——如果一个真人导演会把相邻 2~3 个镜头合成 1 个拍,说明拆得过细,应合并
**一镜到底策略**:当相邻镜头之间存在**动作连续变化、场景轻度变化(同场景内位移)、或拍摄角度渐变**时,可在 `cameraMove``description` 中标注「一镜到底」,将多个碎片镜头合为一个连续运镜长镜头。
- **适用场景**角色行走穿越空间、跟随动作从A点到B点、环绕角色展示环境、定场缓推到主体特写等
- **标注方式**:在 `cameraMove` 中写明运镜路径(如"一镜到底:缓推远景→跟移至院内→落幅全景"),在 `description` 中描述起幅和落幅的画面内容
- **时长放宽**:一镜到底镜头因信息量持续更新,可突破单镜 6s 上限,但不超过 12s
- **风险提示**:一镜到底会提高画面生成的抽卡难度(连续性要求高),仅在叙事流畅性收益明显大于碎切时使用,不滥用
**黄金 6 秒规则**:无台词镜头累计超过 6s 未出现新信息(台词/动作/主体变化),观众注意力断裂。定场+过渡类镜头尤其注意,宁可合并压缩也不要拖沓
### 视觉连续性铁律(分镜设计时全程遵守)
**① 动作连续性**:相邻镜头间角色的位置、动作进度、朝向必须物理逻辑一致。上一镜手伸到半空→下一镜必须从半空状态接续,不能突然收回。
@ -312,11 +335,11 @@ add_deriveAsset({
**emotion**情绪画面传达的情绪基调2~10 字),用具象可感描述。如"冷傲轻蔑""痛苦绝望""紧张压迫"。禁止"开心""难过"等空泛词。
**lighting**光影氛围画面光影与氛围描述5~40 字),须包含**光源方向 + 色调倾向 + 明暗关系**。如"右侧45°冷白光,面部明暗对半,背景深沉""底部暖黄光上打,眼窝沉入暗影"。禁止只写"柔光""暗调"。
**lighting**光影氛围画面光影与氛围描述5~40 字),须包含**光源方向 + 色调倾向 + 明暗关系**。如"右侧冷白光斜射,面部明暗对半,背景深沉""底部暖黄光上打,眼窝沉入暗影"。禁止只写"柔光""暗调"。具体光源角度、色调阶段分配以风格技法参考为准
**scene**:该分镜所处的场景名称,与剧本中的场景对应
**associateAssetsNames**:画面中**可见的**资产名称列表,便于直观确认关联内容
**associateAssetsNames**:画面中**可见的**资产名称列表(包括仅局部出现的角色/物件),便于直观确认关联内容
**duration**:基础参考——特写/表情 2~3s · 对话近景 3~5s · 全身亮相 3~5s · 动作 2~4s · 远景/空镜/过渡 3~5s · 复杂场景 5~8s。**单镜不超过 8s**,超过须拆分。
@ -338,7 +361,10 @@ add_deriveAsset({
**sound**:环境音/音效描述,按「环境音层 + 动作音层」分层。如"远处风声呼啸 + 剑鸣声"。无音效填 `无音效`
**associateAssetsIds**:画面中**可见的**资产的 ID从 assets 数据中获取的实际 `id` 字段值),不编造不存在的 ID
**associateAssetsIds**:画面中**可见的**资产的 ID从 assets 数据中获取的实际 `id` 字段值),不编造不存在的 ID。
- **角色出现即引用**:画面中出现的所有角色,无论是主体还是仅局部可见(如背影、手部、虚化剪影等),只要在画面内可被辨识,都必须引用其对应的资产 ID
- **场景资产必选**:每条分镜必须引用其所处场景对应的场景资产 IDtype 为 scene 的资产);若该场景存在匹配当前画面状态的衍生场景资产,则选用衍生场景资产 ID否则选用主场景资产 ID。缺少场景资产 ID 视为字段不完整
- 父子资产选择规则:按剧情画面所需状态选择资产 ID——若该镜头需要某主资产的衍生状态**只选衍生资产 ID**;仅当不存在匹配的衍生状态时,才选择主资产 ID同一父资产在同一分镜中禁止主/衍生同时出现
### 转场规则
@ -360,9 +386,9 @@ add_deriveAsset({
| 序号 | 画面描述 | 场景 | 关联资产名称 | 时长 | 景别 | 运镜 | 角色动作 | 情绪 | 光影氛围 | 台词 | 音效 | 关联资产ID |
|----|-------------|------|----------|------|------|------|------|------|------|-------|-------|----------|
| 1 | 苏晚卿冷笑,居高临下看着跪地的凌玄,大殿柱影深沉 | 大殿 | [苏晚卿] | 4 | 近景 | 静止 | 嘴角缓缓上扬→微仰下巴→眼神下压注视(开篇) | 冷傲轻蔑 | 顶光直射面部,眼窝明暗对半,背景大殿沉入暗部 | 苏晚卿:还有你当宝贝的青云令 | 空旷殿堂回声 | [101] |
| 2 | 凌玄跪地猛喷鲜血,身体前倾欲坠,血雾弥漫 | 大殿 | [凌玄] | 3 | 中景 | 缓慢推至近景 | 胸口剧颤→猛然喷出鲜血→身体前倾摇晃(承接上镜:跪地状态) | 痛苦绝望 | 左侧冷光勾边,血雾被逆光映成暗红,背景压暗 | 无台词 | 喷血声 + 沉闷跪地声 | [100] |
| 3 | 青云令灵纹一寸寸暗淡,玉面浮现细微裂痕 | 大殿 | [青云令] | 3 | 大特写 | 静止 | 灵纹光芒由亮渐灭→裂痕自中心蔓延(承接上镜:喷血后切物件) | 紧张压迫 | 微弱自发光从内部渗出渐灭,周围完全暗沉 | 无台词 | 细微玉石碎裂声 | [202] |
| 1 | 苏晚卿冷笑,居高临下看着跪地的凌玄,大殿柱影深沉 | 大殿 | [苏晚卿, 凌玄, 大殿] | 4 | 近景 | 静止 | 嘴角缓缓上扬→微仰下巴→眼神下压注视(开篇) | 冷傲轻蔑 | 顶光直射面部,眼窝明暗对半,背景大殿沉入暗部 | 苏晚卿:还有你当宝贝的青云令 | 空旷殿堂回声 | [101, 100, 300] |
| 2 | 凌玄跪地猛喷鲜血,身体前倾欲坠,血雾弥漫 | 大殿 | [凌玄, 大殿] | 3 | 中景 | 缓慢推至近景 | 胸口剧颤→猛然喷出鲜血→身体前倾摇晃(承接上镜:跪地状态) | 痛苦绝望 | 左侧冷光勾边,血雾被逆光映成暗红,背景压暗 | 无台词 | 喷血声 + 沉闷跪地声 | [100, 300] |
| 3 | 青云令灵纹一寸寸暗淡,玉面浮现细微裂痕 | 大殿 | [青云令, 大殿] | 3 | 大特写 | 静止 | 灵纹光芒由亮渐灭→裂痕自中心蔓延(承接上镜:喷血后切物件) | 紧张压迫 | 微弱自发光从内部渗出渐灭,周围完全暗沉 | 无台词 | 细微玉石碎裂声 | [202, 300] |
### 约束
@ -373,13 +399,20 @@ add_deriveAsset({
- **台词原文锁定**:剧本中所有台词必须原文照搬进 `lines` 字段,禁止改写、省略或意译,如有台词未出现在分镜中视为严重错误
- 分镜顺序与剧本叙事顺序一致
- 所有字段完整填写,`associateAssetsIds` 使用资产的实际 ID非数组索引必须与工作区现有资产匹配
- **按剧情选资产(衍生优先)**:同一父资产在单条分镜中,若剧情对应衍生状态则仅填写该衍生资产 ID仅当无匹配衍生状态时才填写主资产 ID禁止两者并填
- **场景资产必须引用**:每条分镜的 `associateAssetsIds` 必须包含该分镜 `scene` 字段对应的场景资产 ID从 assets 中匹配 type 为 scene 的资产);若存在匹配的衍生场景资产则选用衍生 ID否则选用主场景资产 ID。缺少场景资产 ID 视为严重错误
- **角色出现即引用**:画面中出现的所有角色(无论是镜头主体还是仅局部可见——如背影、肢体局部、虚化身影等),只要可被辨识,都必须在 `associateAssetsIds``associateAssetsNames` 中引用其资产。遗漏画面中可见角色的资产 ID 视为严重错误
- 剧本中出现但资产列表不存在的角色/物件仍需在分镜中描述,但不在 `associateAssetsIds` 中编造 ID
- **台词-时长强关联**:含台词的分镜,需根据角色当前情绪状态选取对应语速(愤怒~4字/秒、正常~3字/秒、悲伤~2字/秒、低语/虚弱~2字/秒),`duration` ≥ 台词字数 ÷ 语速(向上取整)+ 1s 情绪余量;宁可多留余量,不可台词超时
- **视觉连续性逐行校验**每写一行分镜前回顾上一行的动作终态、景别、角色朝向确保当前行与之衔接合理符合「视觉连续性铁律」7条规则
- **定场精简**:每个新场景定场最多 1~2 镜,禁止 3 镜以上的碎片化定场;能一镜完成定场+引入的不拆两镜
- **镜头合并自检**:完成全部分镜后,逐段检查是否有可合并的相邻镜头(同空间局部描述、纯装饰镜头、信息重复镜头),合并后重新编号
- **黄金 6 秒**:无台词镜头不超过 6s定场/过渡类镜头尤其注意
- **光影风格一致**:光影描述须与风格技法参考(`director_storyboard_table`)的光影规范保持一致
---
## 五、生成分镜面板与生成分镜图片
## 五、分镜面板写入
### 工具
@ -387,17 +420,277 @@ add_deriveAsset({
|------|------|
| 读取剧本 | `get_flowData("script")` |
| 读取分镜表 | `get_flowData("stoaryTable")` |
### 执行流程
1. 获取 `script``stoaryTable`,并加载下方「分镜提示词 · 通用基础技法」与风格专属技法(激活 `director_storyboard`)作为提示词生成的全部参考依据,冲突时以风格专属技法为准
2. 确定分组与时长规则:同组内分镜 `duration` 累计时长不得超过 15 秒,且每条 `duration` 必须严格使用 `stoaryTable` 对应行时长
3. **人物空间位置预分析**:正式写入前,先通读全部分镜表,梳理同一人物在不同分镜中出现的画面位置与朝向,建立「人物-位置」连续性基准角色A全片画面偏左、面朝右角色B画面偏右、面朝左后续每条 prompt 中涉及该人物时须保持一致
4. **图像资产标注与正文绑定**:为每条分镜的 prompt 生成图像资产标注前缀,按 `associateAssetsIds` 的引用顺序,依次标注 `@图N 为xx{类型}`**提示词正文中所有涉及该角色/场景/道具的位置,必须使用对应的 `@图N` 替代其名称**建立参考图与画面描述的直接绑定详见下方「prompt 图像资产标注规则」)
5. 严格按 `stoaryTable` 的分镜数据行逐行写入分镜面板(排除表头与分隔行),<storyboardItem prompt=提示词内容 track=分组 duration=视频推荐时间 associateAssetsIds="[该分镜所需的资产ID列表]" shouldGenerateImage="是否需要生成分镜图片 true/false, 默认为true" /></storyboardItem>
6. 写入完成后,仅返回一句确认:`已完成分镜面板写入`
### 分镜提示词 · 通用基础技法
> 以下为分镜提示词生成的**通用基础规范**,适用于所有视觉风格。风格锚定词、情绪映射、光影词库、场景质感、美学禁止项等**风格相关内容**由风格专属技法(`director_storyboard`)定义。
#### 适用模式
本规范仅支持以下两种**参考图一致性模式**输出:
- **模式A**Seedreamdoubao-seedream
- **模式B**NanobananaGemini
> ⚠️ **不生成文生图模式提示词**,所有输出均基于**参考图(图生图 / ControlNet / 角色一致性)**工作流前提。
#### 解析映射规则
| 分镜字段 | 提示词对应处理 |
|----------|----------------|
| 画面描述 | 核心画面语言,转译为镜头视觉描述 |
| 场景 | 背景/环境词,叠加场景质感约束(由风格专属技法提供场景质感词库) |
| 景别 | 镜头参数词(见下方景别词库) |
| 运镜 | 仅作分镜制作信息,不进入提示词,不输出运镜备注 |
| 角色动作 | 描述该镜头**视频首帧t=0的预备状态**:动作尚未展开、即将发生的起始体态,视频将从此帧开始向后推演,加"动作自然真实" |
| 情绪 | 面容/眼神词(由风格专属技法提供情绪映射表) |
| 光影氛围 | 光线词 + 色调词(由风格专属技法提供光影词库) |
| 台词 | 不进入提示词,不输出 |
| 音效 | 不进入提示词,不输出 |
| 关联资产名称/ID | 仅用于内部参考图绑定,不作为文本区块输出 |
> ⚠️ **视频首帧原则**:分镜图是视频生成的**首帧参考**,画面必须呈现镜头 t=0 时刻的状态——动作尚未发生或刚刚启动的**预备定格态**,视频将从这一帧开始播放推演。
>
> **核心逻辑**:首帧 → 视频推演 → 动作完成。提示词描述的是"推演起点",而非"推演终点"。
>
> - ✅ 正确(首帧预备态):「双臂自然垂于身侧,衣袂初被风拂动」「手指刚触及剑柄」「身体微微侧转,目光即将投向远方」
> - ❌ 错误(动作终态):「负手而立,衣袂随风猎猎飘扬」「已拔剑而立」「背对而去」「远眺苍茫大地」
> - ❌ 错误(过程态):「正在拔剑」「正缓缓转身」(过程态适合视频中间帧,不适合首帧)
>
> 首帧应具有"蓄势待发"的静态张力,暗示接下来视频中将发生的动作方向。
#### 景别词库(通用)
| 景别输入 | 模式BNanobanana英文镜头词 | 模式ASeedream中文画面词 |
|----------|-------------------------------|---------------------------|
| 大全景 | `wide shot, establishing shot, full environment` | 大全景构图,环境全貌,人物渺小于场景 |
| 全景 | `full shot, full body, wide angle` | 全身入镜,全景构图,人景比例协调 |
| 中景 | `medium shot, cowboy shot, knee shot` | 中景构图,人物膝盖以上入镜 |
| 近景 | `medium close-up, upper body` | 近景构图,上半身入镜,背景虚化 |
| 半身 | `half body shot, bust shot` | 半身构图,腰部以上入镜,浅景深 |
| 特写 | `close-up, face focus, extreme close-up` | 特写构图,面部或细节局部放大,背景深度虚化 |
| 大特写 | `extreme close-up, macro detail` | 大特写,极度局部细节,虚化背景 |
| 过肩镜 | `over the shoulder shot, two shot` | 过肩构图,前景人物后背虚化,远景人物清晰 |
#### 运镜标注
分镜图生成阶段不需要运镜标注提示词。运镜字段仅用于分镜生产管理信息,不参与提示词生成,不作为输出区块。
#### 输出格式规范
每条分镜**只输出一种模式的提示词正文**二选一不允许同条分镜同时输出模式A与模式B。
**模式选择规则**
| 条件 | 选择模式 |
|------|----------|
| 目标模型为 Seedream / 豆包系列 | 模式A中文 Prompt |
| 目标模型为 Nanobanana / Gemini 系列 | 模式B英文 XML Prompt |
| 用户未指定模型 | 默认模式A或询问用户确认 |
| 批量生成 | 全程保持同一模式,不可中途切换 |
**输出内容规则**
- 选择模式A时仅输出 `[Prompt]` 正文无负向词Seedream 不支持)
- 选择模式B时仅输出 `[XML Prompt]` 正文(含 `<negative>` 区块)
- 除提示词正文外,以下内容默认不输出:分镜标题、参考图绑定说明、台词备注、音效备注、约束检查、资产汇总
#### 提示词结构框架
根据目标模型二选一输出:
**模式ASeedreamAPI `reference_images`**
机制:参考图通过 API 参数 `reference_images` 传入prompt 内只写一致性约束语句,不写 URL。
Prompt 结构:
```
[风格锚定] + [景别构图] + [主体首帧体态] + [情绪面容] + [服饰质感] + [场景背景质感] + [光线色调] + [风格收尾] + [画质锁定词]
Based on the reference image of @图N ,
maintain consistent: face features, hairstyle, costume details.
Generate a new scene: [本镜画面描述,使用@图N 替代角色/场景名称].
Keep character appearance identical to reference.
```
> `[风格锚定]``[服饰质感]``[场景背景质感]``[风格收尾]``[画质锁定词]` 的具体内容由**风格专属技法**定义。
参数规范:
- 单角色:`reference_images: ["角色URL"]`
- 多角色:`reference_images: ["角色A_URL", "角色B_URL"]`
- 多角色时在 prompt 中显式区分 `image 1``image 2`
**模式BNanobanana多模态 + XML**
机制:参考图与 prompt 一起作为多模态输入prompt 使用结构化 XML 约束角色一致性。
Prompt 结构(固定框架):
```xml
<role>
You are a cinematographer and storyboard artist.
Maintain strict visual continuity across all shots.
</role>
<character_reference>
Image [1]: @图1 — [外貌关键描述: 发色/发型/服装/体型]
Image [2]: @图2 — [外貌关键描述]
</character_reference>
<continuity_rules>
- Same wardrobe, hairstyle, face features across ALL shots
- Same environment, lighting style, color grade
- Only framing, angle, action, expression may change
- Do NOT introduce new characters not in reference images
</continuity_rules>
<shot>
[本镜分镜提示词:景别/构图/动作/情绪/光线/场景质感]
[画质锁定词]
(具体内容由风格专属技法定义)
</shot>
<negative>
[负向词模板]
(具体词条由风格专属技法定义)
</negative>
```
参数规范:
- 参考图作为图片输入,不是 URL 文本
- 角色描述保持 1-2 句关键特征,避免冗长
- 仅允许改变景别、角度、动作、表情,不改变人物身份特征
#### 通用语言与质量规范
- 模式ASeedream优先中文自然语言段落
- 模式BNanobanana优先英文 XML 结构化提示词
- 提示词聚焦"内容表现 + 画质锐利",避免模糊类词
- 不使用会导致糊图的表达(见下方「画质降级禁用词」表)
- 模式B 负向词按风格专属「负向词模板」输出每条必须包含不可省略模式A 不输出负向词
- 画质锁定词按风格专属「画质锁定词」模板输出,每条必须包含
#### 画外文字 vs 画内文字规则
- **画外文字**(字幕、水印、标题卡、旁白叠字等 UI 层覆盖文字)→ **绝对禁止**,必须在画质锁定词和负向词中声明禁止
- **画内文字**(场景中自然存在的文字道具:角色提笔写字、书卷上的字迹、匾额牌匾、书信内容、路标、店铺招牌等)→ **属于场景道具**,当分镜画面描述中明确包含此类内容时,应正常描述其存在,不受禁止文字规则限制
- **判断标准**:该文字是否存在于**故事世界内部**。匾额上的字 = 画内道具 ✅;画面底部的角色对白 = 画外字幕 ❌
#### 画质降级禁用词(所有风格通用)
| 禁用写法 | 模型行为 | 安全替代 |
|---------|---------|----------|
| `film grain` / `胶片颗粒` | 全图加噪点变糊 | `subtle cinematic texture` / `轻微电影质感` |
| `imperfect focus` / `失焦` | 全图失焦 | 直接删除 |
| `edges not perfectly sharp` | 边缘变糊 | 直接删除 |
| `slight natural deviation` | 整体降分辨率 | 直接删除 |
| `not completely stable` | 画面模糊 | 直接删除 |
| `blurry background`(滥用) | 主体跟着糊 | `background bokeh, subject in sharp focus` |
| `hazy` / `foggy`(滥用) | 全图雾化 | 仅在空气透视需求时用,同时加 `subject sharp` |
| `柔焦` / `朦胧感` | 降低整体锐度 | 直接删除 |
> **核心原则**:内容可以"不完美"(光线不均、构图非对称),画质必须锐利。
#### 批量处理规范
用户输入多行分镜表时:
1. **逐行顺序处理**,不跳行、不合并
2. 每条分镜仅输出目标模式的提示词正文Prompt 或 XML Prompt
3. 若同一场景连续多镜,**场景质感词可复用**,但情绪/光线/景别/动作必须**按行独立处理**
4. 关联资产名称相同的镜次,**一致性标注词必须一致**
5. 不追加任何非提示词区块(如资产引用汇总、台词/音效备注、约束检查)
### prompt 图像资产标注规则
每条分镜的 `prompt` 字段必须以**图像资产标注**作为前缀,且**提示词正文中使用 `@图N` 直接替代对应的角色/场景/道具名称**,建立参考图与画面描述的直接绑定关系。标注按 `associateAssetsIds` 中资产的引用顺序,从 `@图1` 开始依次编号。
**格式**`@图1 为{资产名称}{资产类型} @图2 为{资产名称}{资产类型} ... , 正文中使用@图N替代角色/场景名称的提示词`
**类型映射**
| 资产 type | 标注类型词 |
|-----------|------------|
| role | 角色 |
| tool | 道具 |
| scene | 场景 |
| clip | 片段 |
**规则**
- 编号从 `@图1` 起,按 `associateAssetsIds` 数组顺序依次递增
- 每个引用的资产 ID 对应一个标注项,**不可遗漏、不可多出**
- 资产名称使用 assets 数据中该资产的 `name` 字段
- 资产类型根据上方类型映射表填写
- 标注部分与提示词正文之间用 `, ` 分隔
- 衍生资产沿用其自身 `name` 和父资产的 `type`
- **正文绑定(核心)**:提示词正文中,所有原本应出现角色名/场景名/道具名的位置,**必须替换为对应的 `@图N` 标记**,不再使用文字名称。这样参考图与画面中的视觉主体形成直接指向关系,避免资产名称与角色名称不一致导致的歧义(如衍生资产名"幕离红斗篷"与角色名"戚映竹"无法对应的问题)
- 同一 `@图N` 在正文中可多次出现(如角色在前景和反射面中同时可见时)
**示例**(假设 `associateAssetsIds="[101, 100, 300]"` 对应苏晚卿(role)、凌玄(role)、大殿(scene)
❌ 错误(正文使用文字名称,与前缀标注脱节):
```
@图1 为苏晚卿角色 @图2 为凌玄角色 @图3 为大殿场景, 苏晚卿冷笑,居高临下看着跪地的凌玄,大殿柱影深沉……
```
✅ 正确(正文使用 @图N 直接绑定参考图):
```
@图1 为苏晚卿角色 @图2 为凌玄角色 @图3 为大殿场景, @图1 冷笑,居高临下看着跪地的@图2@图3 柱影深沉……
```
### prompt 人物位置连贯性规则
生成每条 prompt 时,须遵守以下跨分镜人物位置一致性约束:
- **画面位置锁定**:同一角色在同一场景内的多条分镜中,其画面左右位置(画面左侧 / 中央 / 右侧)须保持固定,不得无叙事理由地跳侧
- **朝向守恒**:对话/对峙场景遵循 180° 视轴线——角色A面朝右则全场景保持面朝右角色B面朝左则全场景保持面朝左prompt 中须通过方位词facing left / 面朝左、on the left side of frame / 画面左侧等)显式标注
- **前后景层次一致**若角色A在分镜N中处于前景、角色B处于中景则同场景后续分镜中二者前后关系不应无理由反转
- **位置变化须有动作衔接**:角色画面位置确需变化时(如角色走动、转身),前序分镜的 prompt 中须包含对应位移/转身动作描写,不可凭空跳位
- **跨场景可重置**:切换到全新场景时允许重新分配画面位置,但新场景内部仍须保持一致
- **反射面视觉关系**:当画面中存在反射介质(镜面、水面、光滑金属、窗玻璃、相机镜头等)时,须注意以下规则:
- **镜像翻转**反射面中角色的左右朝向与实体相反实体面朝右→镜像面朝左prompt 中须显式标注反射体与实体的朝向关系(如"@图1 面朝右,水面倒影中@图1 面朝左"
- **反射面不改变位置基准**:角色的画面位置以实体为准,反射面中的映像不视为角色位置变化
- **反射面内容与实体一致**:反射面中可见的角色服饰、发型、表情等必须与同帧实体一致,不可出现偏差
- **反射面景深与清晰度**:根据反射面距离和材质,反射图像可适当降低清晰度(如水面波纹导致的模糊),但须在 prompt 中标注(如"水面倒影微微扭曲"
- **识别触发**:当分镜画面描述或场景资产中包含镜面、水面、湖面、溪流、玻璃、金属反光、相机/摄像等反射性元素时,自动触发本规则
### 约束
- 前置条件:分镜表已构建完成且用户已确认
- 你必须使用XML格式写入工作区分镜面板<storyboardItem videoPrompt='视频提示词' prompt='提示词内容' track='分组' duration='视频推荐时间' associateAssetsIds="[该分镜所需的资产ID列表]" shouldGenerateImage="是否需要生成分镜图片 true/false, 默认为true" ></storyboardItem>
- 分组总时长约束:每个 `group` 的累计时长不得超过 15 秒
- 行数一致性约束:分镜面板 `items` 数量必须与 `stoaryTable` 的分镜数据行数量完全一致(不包含表头与分隔行)
- 时长一致性约束:分镜面板 `duration` 必须与 `stoaryTable` 对应行时长完全一致
- **人物位置连贯性**:每条 prompt 须通过上述「人物位置连贯性规则」校验,同场景内同一人物的画面位置与朝向描述前后一致
- **图像资产标注必填**:每条 prompt 必须以图像资产标注前缀开头,标注数量与 `associateAssetsIds` 数量一致、顺序一致;缺少标注或顺序不匹配视为格式错误
- 阶段边界:本阶段禁止调用 `generate_storyboard_images`
---
## 六、分镜图生成
### 工具
| 操作 | 调用 |
|------|------|
| 读取分镜面板 | `get_flowData("storyboard")` |
| 生成图片 | `generate_storyboard_images({ ids: [分镜ID列表] })` |
### 执行流程
1. 获取 `script``stoaryTable`
2. 使用XML格式写入工作区分镜面板<storyboardItem prompt=提示词内容 track=分组 duration=视频推荐时间 associateAssetsIds="[该分镜所需的资产ID列表]" shouldGenerateImage="是否需要生成分镜图片 true/false, 默认为true" /></storyboardItem>
3. 生成分镜面板后,先获取 `storyboard`数据 再调用 `generate_storyboard_images({ ids: [真实分镜ID列表] })` 生成分镜图片(异步,发起即返回)
1. 获取 `storyboard`
2. 提取真实分镜 ID 列表
3. 调用 `generate_storyboard_images({ ids: [真实分镜ID列表] })` 生成分镜图片(异步,发起即返回)
### 约束
- 前置条件:分镜表已构建完成且用户已确认
- 前置条件:分镜面板已写入完成
- 图片必须与分镜描述匹配
- 你必须使用XML格式写入工作区分镜面板<storyboardItem prompt=提示词内容 track=分组 duration=视频推荐时间 associateAssetsIds="[该分镜所需的资产ID列表]" shouldGenerateImage="是否需要生成分镜图片 true/false, 默认为true" /></storyboardItem>
- 仅使用 `storyboard` 中的真实分镜 ID禁止编造或复用无效 ID

View File

@ -138,7 +138,7 @@ function createSubAgent(parentCtx: AgentContext) {
"你必须使用如下XML格式写入工作区\n```",
"拍摄计划:<scriptPlan>内容</scriptPlan>",
"分镜表:<storyboardTable>内容</storyboardTable>",
"分镜面板:<storyboardItem prompt=提示词内容 track=分组 duration=视频推荐时间 associateAssetsIds=[该分镜所需的资产ID列表] /></storyboardItem>",
"分镜面板:<storyboardItem videoPrompt='视频提示词' prompt=提示词内容 track='分组' duration='视频推荐时间' associateAssetsIds='[该分镜所需的资产ID列表]'></storyboardItem>",
"```",
].join("\n");
// "剧本:<script>内容</script>",

View File

@ -488,6 +488,8 @@ description: 专注于从剧本内容中提取所使用的资产(角色、场
table.text("state");
table.integer("trackId");
table.text("reason");
table.text("videoPrompt");
table.integer("shouldGenerateImage"); // 0 否 1 是
table.integer("projectId");
table.integer("flowId"); //工作流id
table.integer("index");
@ -539,6 +541,7 @@ description: 专注于从剧本内容中提取所使用的资产(角色、场
table.text("reason");
table.text("prompt");
table.integer("selectVideoId");
table.integer("duration");
table.primary(["id"]);
table.unique(["id"]);
},

View File

@ -18,12 +18,14 @@ export default router.post(
prompt: z.string(),
duration: z.number(),
state: z.string(),
videoPrompt: z.string(),
shouldGenerateImage: z.number(),
src: z.string().nullable(),
scriptId: z.number(),
projectId: z.number(),
}),
async (req, res) => {
const { prompt, duration, state, src, scriptId, projectId } = req.body;
const { prompt, duration, state, src, scriptId, projectId, videoPrompt, shouldGenerateImage } = req.body;
const [trackId] = await u.db("o_videoTrack").insert({
scriptId: scriptId,
@ -35,6 +37,8 @@ export default router.post(
state,
filePath: new URL(src).pathname,
trackId,
videoPrompt,
shouldGenerateImage,
scriptId: scriptId,
projectId: projectId,
});

View File

@ -4,14 +4,6 @@ import { z } from "zod";
import { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
interface Storyboard {
id: number;
track: string;
src: string | null;
associateAssetsIds: number[];
duration: number;
state: string;
}
export default router.post(
"/",
validateFields({
@ -22,6 +14,8 @@ export default router.post(
track: z.string(),
state: z.string(),
src: z.string().nullable(),
videoPrompt: z.string(),
shouldGenerateImage: z.number(),
associateAssetsIds: z.array(z.number()),
}),
),
@ -38,6 +32,9 @@ export default router.post(
state: item.state,
scriptId,
projectId,
track:item.track,
videoPrompt:item.videoPrompt,
shouldGenerateImage:item.shouldGenerateImage,
createTime: Date.now(),
});
if (item.associateAssetsIds?.length) {
@ -50,33 +47,47 @@ export default router.post(
}
item.id = id;
}
const lastStoryboard = await u.db("o_storyboard").where("scriptId", scriptId);
if (!lastStoryboard || !lastStoryboard.length) return res.status(400).send(error("未查到分镜数据"));
//根据track分组
const storyboardGroupByTrack: Record<string, number[]> = {};
data.forEach((item: any) => {
lastStoryboard.forEach((item: any) => {
if (!storyboardGroupByTrack[item.track]) {
storyboardGroupByTrack[item.track] = [];
}
storyboardGroupByTrack[item.track].push(item.id);
});
//循环
//循环先查询数据库中是否已存在相同track名称的trackId有则复用没有则新建
for (const track in storyboardGroupByTrack) {
const [trackId] = await u.db("o_videoTrack").insert({
scriptId,
projectId,
});
const storyboardIds = storyboardGroupByTrack[track] ?? [];
// 计算该track下所有分镜的duration总和
const trackDuration = lastStoryboard
.filter((item: any) => item.track == track)
.reduce((sum: number, item: any) => sum + Number(item.duration), 0);
// 查找该scriptId下是否已有相同track名称且已分配trackId的分镜记录
const existingStoryboard = await u.db("o_storyboard").where({ scriptId, track }).whereNotNull("trackId").first();
let trackId: number;
if (existingStoryboard?.trackId) {
// 已存在相同track名称的trackId直接复用并更新duration
trackId = existingStoryboard.trackId;
await u.db("o_videoTrack").where("id", trackId).update({ duration: trackDuration });
} else {
// 不存在新建videoTrack
const [newTrackId] = await u.db("o_videoTrack").insert({
scriptId,
projectId,
duration: trackDuration,
});
trackId = newTrackId;
}
await u.db("o_storyboard").whereIn("id", storyboardIds).update({ trackId });
}
const lastStoryboard = await u
.db("o_storyboard")
.where("scriptId", scriptId)
.select("id", "trackId", "prompt", "duration", "state", "scriptId", "reason", "filePath");
if (!lastStoryboard || !lastStoryboard.length) return res.status(400).send(error("未查到分镜数据"));
batchGenerateVideoPrompts(
data.map((i: any) => i.id),
projectId,
);
const storyboardData = await Promise.all(
lastStoryboard.map(async (i) => {
return {
@ -95,58 +106,3 @@ export default router.post(
return res.status(200).send(success(storyboardData));
},
);
async function batchGenerateVideoPrompts(storyboardIds: number[], projectId: number) {
const lastStoryboard = await u.db("o_storyboard").whereIn("id", storyboardIds).select("id", "trackId", "prompt");
const allTrackIds = lastStoryboard.map((i) => i.trackId);
const storyboardPromptRecord: Record<number, string[]> = {};
lastStoryboard.forEach((i) => {
if (i.trackId) {
if (!storyboardPromptRecord[i.trackId]) {
storyboardPromptRecord[i.trackId] = [];
}
storyboardPromptRecord[i.trackId].push(i.prompt!);
}
});
const projectSetting = await u.db("o_project").where("id", projectId).select("artStyle").first();
const systemPrompt = u.getArtPrompt(projectSetting?.artStyle!, "art_storyboard_video");
await u
.db("o_videoTrack")
.whereIn("id", allTrackIds as number[])
.update({
state: "生成中",
});
for (const trackId in storyboardPromptRecord) {
const storboardPrompts = storyboardPromptRecord[trackId];
try {
const { text } = await u.Ai.Text("universalAi").invoke({
messages: [
{
role: "system",
content: systemPrompt,
},
{
role: "user",
content: `请根据我所提供的 ${storboardPrompts.length} 条分镜内容,为我生成一条视频提示词,请直接输出提示词内容,不做任何解释说明。
:
${storboardPrompts.map((i, index) => `${index + 1}.${i}`).join("\n")}`,
},
],
});
await u.db("o_videoTrack").where("id", trackId).update({
state: "已完成",
prompt: text,
});
console.log("%c Line:116 🍎 text", "background:#42b983", text);
} catch (e) {
console.error("生成视频提示词失败", e);
await u
.db("o_videoTrack")
.where("id", trackId)
.update({
state: "生成失败",
reason: u.error(e).message,
});
}
}
}

View File

@ -43,6 +43,8 @@ export default router.post(
.leftJoin("o_assets2Storyboard", "o_assets.id", "o_assets2Storyboard.assetId")
.whereIn("o_assets2Storyboard.storyboardId", finalStoryboardIds)
.select("o_assets2Storyboard.storyboardId", "o_assets.imageId");
console.log("%c Line:42 🥪 assetData", "background:#ea7e5c", assetData);
const assetRecord: Record<number, number[]> = {};
assetData.forEach((item: any) => {
if (!assetRecord[item.storyboardId]) {
@ -50,9 +52,7 @@ export default router.post(
}
assetRecord[item.storyboardId].push(item.imageId);
});
await u.db("o_storyboard").whereIn("id", finalStoryboardIds).update({
state: "生成中",
});
res.status(200).send(
success(
storyboardData.map((i) => ({
@ -111,25 +111,30 @@ export default router.post(
},
);
async function getAssetsImageBase64(imageIds: number[]) {
if (imageIds.length === 0) return [];
const imagePaths = await u
.db("o_assets")
.leftJoin("o_image", "o_assets.imageId", "o_image.id")
.whereIn("o_assets.id", imageIds)
.select("o_assets.id", "o_image.filePath");
if (!imagePaths.length) return [];
if (!imageIds.length) return [];
const imagePaths = await u.db("o_image").whereIn("o_image.id", imageIds).select("o_image.id", "o_image.filePath");
// 建立 id 到 filePath 的映射
const id2Path = new Map<number, string>();
for (const row of imagePaths) {
id2Path.set(row.id, row.filePath);
}
// 保证输出顺序与 imageIds 一致
const imageUrls = await Promise.all(
imagePaths.map(async (i) => {
if (i.filePath) {
imageIds.map(async (id) => {
const filePath = id2Path.get(id);
if (filePath) {
try {
return await urlToBase64(await u.oss.getFileUrl(i.filePath));
return await urlToBase64(await u.oss.getFileUrl(filePath));
} catch {
return null;
}
} else {
return null;
}
return null;
}),
);
// 保留顺序,并且过滤掉无效项
return imageUrls.filter(Boolean) as string[];
}

View File

@ -1,6 +1,43 @@
// @db-hash c0d74bd27b3a41b397705c93d1737a3b
// @db-hash 147d0f569132c3ba4fedb17a1039d15f
//该文件由脚本自动生成,请勿手动修改
export interface _o_storyboard_old_20260402 {
'createTime'?: number | null;
'duration'?: string | null;
'filePath'?: string | null;
'flowId'?: number | null;
'id'?: number;
'index'?: number | null;
'projectId'?: number | null;
'prompt'?: string | null;
'reason'?: string | null;
'scriptId'?: number | null;
'state'?: string | null;
'trackId'?: number | null;
}
export interface _o_vendorConfig_old_20260401 {
'author'?: string | null;
'code'?: string | null;
'createTime'?: number | null;
'description'?: string | null;
'enableEnglish'?: number | null;
'icon'?: string | null;
'id'?: string;
'inputs'?: string | null;
'inputValues'?: string | null;
'models'?: string | null;
'name'?: string | null;
}
export interface _o_videoTrack_old_20260402 {
'id'?: number;
'projectId'?: number | null;
'prompt'?: string | null;
'reason'?: string | null;
'scriptId'?: number | null;
'selectVideoId'?: number | null;
'state'?: string | null;
'videoId'?: number | null;
}
export interface memories {
'content': string;
'createTime': number;
@ -173,8 +210,11 @@ export interface o_storyboard {
'prompt'?: string | null;
'reason'?: string | null;
'scriptId'?: number | null;
'shouldGenerateImage'?: number | null;
'state'?: string | null;
'track'?: string | null;
'trackId'?: number | null;
'videoPrompt'?: string | null;
}
export interface o_tasks {
'describe'?: string | null;
@ -197,6 +237,7 @@ export interface o_vendorConfig {
'code'?: string | null;
'createTime'?: number | null;
'description'?: string | null;
'enable'?: number | null;
'enableEnglish'?: number | null;
'icon'?: string | null;
'id'?: string;
@ -216,17 +257,20 @@ export interface o_video {
'videoTrackId'?: number | null;
}
export interface o_videoTrack {
'duration'?: number | null;
'id'?: number;
'projectId'?: number | null;
'prompt'?: string | null;
'reason'?: string | null;
'scriptId'?: number | null;
'selectVideoId'?: number | null;
'state'?: string | null;
'videoId'?: number | null;
}
export interface DB {
"_o_storyboard_old_20260402": _o_storyboard_old_20260402;
"_o_vendorConfig_old_20260401": _o_vendorConfig_old_20260401;
"_o_videoTrack_old_20260402": _o_videoTrack_old_20260402;
"memories": memories;
"o_agentDeploy": o_agentDeploy;
"o_agentWorkData": o_agentWorkData;