This commit is contained in:
ACT丶流星雨 2026-04-02 20:59:09 +08:00
commit 63881df879
54 changed files with 1353 additions and 324 deletions

View File

@ -1,3 +1,6 @@
123水电费水电费
123
123
123
1212121212的王师傅水电费第三方水电费
1212121212

View File

@ -114,11 +114,20 @@
| 审核 | 不需要 |
**决策层行为:**
向执行层派发阶段5分镜面板写入任务收到确认后进入阶段6。
阶段4完成后、派发阶段5之前根据模型参数 `多参` 决定写入模式:
| 模型参数 `多参` | 决策层操作 |
|----------------|-----------|
| 是 | 向用户询问:使用 **"纯文本多参模式"** 还是 **"分镜图辅助多参模式"**,等待用户确认后,将所选模式随任务指令一起派发给执行层 |
| 否 | 无需询问用户,直接以 **"首位帧模式"** 派发给执行层 |
收到执行层完成确认后进入阶段6。
**阶段特有约束:**
- 必须严格依据阶段4分镜表逐行写入行数与时长保持一致
- 分组累计时长不得超过 15 秒
- 派发执行层时必须在指令中明确携带写入模式(纯文本多参模式 / 分镜图辅助多参模式 / 首位帧模式)
---

View File

@ -52,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", // 衍生资产类型
})
```
@ -66,10 +66,10 @@ add_deriveAsset({
- `name`2~6 字,体现视觉外观变化
- `desc``[与默认态的差异] · [视觉特征] 1~100 字
- `type`
- 角色资产填 `role`
- 道具资产填 `tool`
- 场景资产填 `scene`
- 镜头/片段类资产填 `clip`
- 角色资产填 `role`
- 道具资产填 `tool`
- 场景资产填 `scene`
- 镜头/片段类资产填 `clip`
@ -133,7 +133,7 @@ add_deriveAsset({
### 执行流程
1. 加载风格技法参考,获取 `script``assets`,并并且激活 `director_planning` ,所有规划内容以该文档为风格基准,冲突时以风格技法参考为准。
1. 加载风格技法参考,获取 `script``assets`,并并且激活 `director_planning_narrative` 以及 `director_planning_style`,所有规划内容以该文档为风格基准,冲突时以风格技法参考为准。
2. 按下方规范制定导演规划(创作规划),全文遵守「导演具象化原则」
### 导演具象化原则(贯穿全文)
@ -163,7 +163,7 @@ add_deriveAsset({
约束:
- 色调具体到色温范围或色彩倾向描述
- 光影以「段落-光影方向」表格呈现,每段落指定光影基调方向
- 色温、光源角度、冷暖色调分配等具体技法参数以风格技法参考(`director_planning`)为准
- 色温、光源角度、冷暖色调分配等具体技法参数以风格技法参考(`director_planning_narrative` 以及 `director_planning_style`)为准
- **构图须说明叙事理由**,参考以下情绪-构图映射(按需选用):
- 对称构图 → 秩序 / 压迫 / 庄重
- 三分法偏侧留白 → 孤独 / 期待 / 未知
@ -210,7 +210,7 @@ add_deriveAsset({
约束:
- 配乐按段落统一规划(不逐场),同段落内场景切换靠环境音变化过渡
- 乐器选择、组合策略等具体技法以风格技法参考(`director_planning`)为准
- 乐器选择、组合策略等具体技法以风格技法参考(`director_planning_narrative` 以及 `director_planning_style`)为准
- 环境音具体到可感知声源("蝉鸣 / 溪水 / 市井叫卖 / 雨滴檐角"),每场标注 1~2 个核心环境音
- 标注运用沉默手法的关键瞬间(关键情感瞬间优先考虑去掉配乐,只留环境音)
- 全片配乐覆盖率建议不超过 70%,留白段落与配乐段落形成呼吸感
@ -249,7 +249,7 @@ add_deriveAsset({
### 执行流程
1. 获取 `script``assets`,并且激活 `director_storyboard_table` ,作为分镜设计的风格参考。
1. 获取 `script``assets`,并且激活 `director_storyboard_table_narrative` 以及 `director_storyboard_table_style` ,作为分镜设计的风格参考。
2. 按下方规则将剧本拆分为分镜,**每写一行前**回顾上一行状态,确保符合「视觉连续性铁律」后再填写当前行所有字段
### 分镜拆分原则
@ -408,7 +408,7 @@ add_deriveAsset({
- **定场精简**:每个新场景定场最多 1~2 镜,禁止 3 镜以上的碎片化定场;能一镜完成定场+引入的不拆两镜
- **镜头合并自检**:完成全部分镜后,逐段检查是否有可合并的相邻镜头(同空间局部描述、纯装饰镜头、信息重复镜头),合并后重新编号
- **黄金 6 秒**:无台词镜头不超过 6s定场/过渡类镜头尤其注意
- **光影风格一致**:光影描述须与风格技法参考(`director_storyboard_table`)的光影规范保持一致
- **光影风格一致**:光影描述须与风格技法参考(`director_storyboard_table_narrative` 以及 `director_storyboard_table_style`)的光影规范保持一致
---
@ -421,14 +421,34 @@ add_deriveAsset({
| 读取剧本 | `get_flowData("script")` |
| 读取分镜表 | `get_flowData("stoaryTable")` |
### 写入模式
本阶段根据决策层派发指令中携带的模式信息,选择对应的写入策略:
| 模式 | 说明 | prompt | shouldGenerateImage | track 分组规则 |
|------|------|--------|---------------------|----------------|
| **纯文本多参模式** | 仅写入视频描述与资产绑定,不生成提示词和分镜图 | `''`(空字符串) | `false` | 同「分镜图辅助多参模式」,累计时长 ≤ 15s |
| **分镜图辅助多参模式** | 完整生成提示词并生成分镜图(当前默认行为) | 正常生成 | `true`(默认) | 累计时长 ≤ 15s |
| **首位帧模式** | 完整生成提示词,每条分镜独立一组 | 正常生成 | `true`(默认) | **不分组**,每行独立一组,按顺序递增 |
> 模式信息由决策层在派发指令中明确指定,执行层不自行判断。
### 执行流程
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. 写入完成后,仅返回一句确认:`已完成分镜面板写入`
1. 获取 `script``stoaryTable`,识别决策层指令中的**写入模式**(纯文本多参模式 / 分镜图辅助多参模式 / 首位帧模式)
2. **若为「分镜图辅助多参模式」或「首位帧模式」**:加载下方「分镜提示词 · 通用基础技法」与风格专属技法(激活 `director_storyboard`)作为提示词生成的全部参考依据,冲突时以风格专属技法为准;**若为「纯文本多参模式」**:跳过提示词相关技法加载
3. 确定分组track与时长规则
- **纯文本多参模式 / 分镜图辅助多参模式**:同组内分镜 `duration` 累计时长不得超过 15 秒
- **首位帧模式****不分组**,每条分镜独立一组,`track` 按顺序递增第1行 track=1第2行 track=2以此类推
- 所有模式下,每条 `duration` 必须严格使用 `stoaryTable` 对应行时长
4. **人物空间位置预分析**(纯文本多参模式跳过此步):正式写入前,先通读全部分镜表,梳理同一人物在不同分镜中出现的画面位置与朝向,建立「人物-位置」连续性基准角色A全片画面偏左、面朝右角色B画面偏右、面朝左后续每条 prompt 中涉及该人物时须保持一致
5. **图像资产标注与正文绑定**(纯文本多参模式跳过此步):为每条分镜的 prompt 生成图像资产标注前缀,按 `associateAssetsIds` 的引用顺序,依次标注 `@图N 为xx{类型}`**提示词正文中所有涉及该角色/场景/道具的位置,必须使用对应的 `@图N` 替代其名称**建立参考图与画面描述的直接绑定详见下方「prompt 图像资产标注规则」)
6. **生成视频描述videoDesc**(所有模式均需):根据 `stoaryTable` 对应行的完整分镜数据画面描述、场景、关联资产名称、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效、关联资产ID将该行信息整合为一段结构化的视频描述文本填入 `videoDesc` 字段
7. 严格按 `stoaryTable` 的分镜数据行逐行写入分镜面板(排除表头与分隔行),根据模式差异化输出:
- **纯文本多参模式**`<storyboardItem videoDesc='视频描述' prompt='' track='分组' duration='视频推荐时间' associateAssetsIds="[该分镜所需的资产ID列表]" shouldGenerateImage="false" ></storyboardItem>`
- **分镜图辅助多参模式**`<storyboardItem videoDesc='视频描述' prompt='提示词内容' track='分组' duration='视频推荐时间' associateAssetsIds="[该分镜所需的资产ID列表]" shouldGenerateImage="true" ></storyboardItem>`
- **首位帧模式**`<storyboardItem videoDesc='视频描述' prompt='提示词内容' track='按顺序递增的独立分组' duration='视频推荐时间' associateAssetsIds="[该分镜所需的资产ID列表]" shouldGenerateImage="true" ></storyboardItem>`
8. 写入完成后,仅返回一句确认:`已完成分镜面板写入({当前模式名称}`
### 分镜提示词 · 通用基础技法
@ -664,14 +684,23 @@ Image [2]: @图2 — [外貌关键描述]
### 约束
- 前置条件:分镜表已构建完成且用户已确认
- 你必须使用XML格式写入工作区分镜面板<storyboardItem videoDesc='视频描述' prompt='提示词内容' track='分组' duration='视频推荐时间' associateAssetsIds="[该分镜所需的资产ID列表]" shouldGenerateImage="是否需要生成分镜图片 true/false, 默认为true"></storyboardItem>
- 分组总时长约束:每个 `group` 的累计时长不得超过 15 秒
- 你必须使用XML格式写入工作区分镜面板具体参数值按当前模式填写见上方执行流程第7步
- **videoDesc 必填**(所有模式):每条分镜的 `videoDesc` 必须根据 `stoaryTable` 对应行的分镜数据生成包含画面描述、场景、关联资产名称、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效、关联资产ID 等完整信息
- 行数一致性约束:分镜面板 `items` 数量必须与 `stoaryTable` 的分镜数据行数量完全一致(不包含表头与分隔行)
- 时长一致性约束:分镜面板 `duration` 必须与 `stoaryTable` 对应行时长完全一致
- **人物位置连贯性**:每条 prompt 须通过上述「人物位置连贯性规则」校验,同场景内同一人物的画面位置与朝向描述前后一致
- **图像资产标注必填**:每条 prompt 必须以图像资产标注前缀开头,标注数量与 `associateAssetsIds` 数量一致、顺序一致;缺少标注或顺序不匹配视为格式错误
- 阶段边界:本阶段禁止调用 `generate_storyboard_images`
**模式差异化约束:**
| 约束项 | 纯文本多参模式 | 分镜图辅助多参模式 | 首位帧模式 |
|--------|---------------|-------------------|------------|
| `prompt` | `''`(空字符串) | 正常生成提示词 | 正常生成提示词 |
| `shouldGenerateImage` | `false` | `true` | `true` |
| `track` 分组 | 累计时长 ≤ 15s | 累计时长 ≤ 15s | 每行独立一组,按顺序递增 |
| 人物位置连贯性校验 | 不适用(无 prompt | **必须**校验 | **必须**校验 |
| 图像资产标注 | 不适用(无 prompt | **必填** | **必填** |
| 提示词技法加载 | 跳过 | 加载通用基础技法 + 风格专属技法 | 加载通用基础技法 + 风格专属技法 |
---
## 六、分镜图生成

View File

@ -172,11 +172,11 @@ description: >-
| 审核项 | 标准 | 严重程度 |
|--------|------|----------|
| 关联资产正确 | associateAssetsIds 中的索引均在 assets 数组范围内;画面中可见的资产已关联 | 严重 |
| 父子资产选择正确 | 同一分镜按剧情优先选择衍生资产 ID无匹配衍生时才使用主资产 ID且二者不得同时出现 | 严重 |
| 剧本覆盖度 | 剧本中的全部场景和关键事件均有对应分镜,无遗漏 | 严重 |
| 拆分粒度 | 一个独立画面对应一条分镜;无过度合并或过度拆分 | 中等 |
| 镜头语言合理 | camera 字段使用标准景别术语;景别变化服务于叙事节奏 | 中等 |
| 时长合理性 | duration 与画面复杂度匹配;总时长与剧本预估时长基本吻合 | 中等 |
| frameMode 选择 | 帧模式与分镜内容匹配(动作结果用 endFrame、对话为主用 linesSoundEffects、其余用 firstFrame | 轻微 |
### 详细审核标准
@ -200,6 +200,19 @@ description: >-
- assets 只有 3 个,但分镜中出现 `associateAssetsIds: [1, 5]`
- description 描述"凌玄手持青云令",但 associateAssetsIds 只有凌玄的索引,遗漏了青云令
#### 父子资产选择正确(严重)
验证方法:
1. 基于 assets 建立 `deriveId -> assetsId(父资产ID)` 映射
2. 遍历每条分镜 `associateAssetsIds`
3. 结合分镜 `description` 判断当前镜头是否明确为衍生状态(如破损、染血、夜景版、激活态等)
4. 若为衍生状态却只填父 `assetsId`,或同时出现 `deriveId` 与父 `assetsId`,均判定不通过
5. 若该镜头无匹配衍生状态,允许且应使用主 `assetsId`
不通过示例:
- 同一分镜 `associateAssetsIds: [1001, 101]`,其中 `1001``101` 的衍生资产
- description 明确“青云令裂痕发光(激活态)”,但 `associateAssetsIds` 仅填写主资产 `101`,未选择对应衍生资产 ID
#### 剧本覆盖度(严重)
验证方法:

View File

@ -25,23 +25,23 @@
### 项目参数表
| 参数 | 说明 | 示例 |
|------|------|------|
| 集数 | 总共拆分为几集 | 7集 |
| 单集时长 | 每集目标时长(分钟) | 2.5分钟 |
| 原著范围 | 改编覆盖的章节范围 | 第1-35章 |
| 章节ID列表 | 本次任务涉及的章节ID用于事件检索 | [1,2,3,4,5] |
| 平台规格 | 画面比例(竖屏/横屏) | 竖屏9:16 |
| 风格定位 | 短剧整体风格标签 | 诡异修仙+心理悬疑 |
| 付费策略 | 前几集免费、从第几集设付费点 | 前2集免费第3集起付费 |
| 参数 | 说明 |
|------|------|
| 集数 | 总共拆分为几集 |
| 单集时长 | 每集目标时长(分钟) |
| 原著范围 | 改编覆盖的章节范围 |
| 平台规格 | 画面比例(竖屏/横屏) |
| 风格定位 | 短剧整体风格标签 |
| 付费策略 | 前几集免费、从第几集设付费点 |
### 初始化对话流程
1. 用户发起改编请求时,**必须主动询问用户**项目参数(不主动调用 `deepRetrieve`,除非用户要求回想之前的配置)
2. 如果没有已确认的参数,**必须主动询问用户**
- "请确认以下信息:计划拆分为几集?每集大约几分钟?覆盖原著哪些章节?"
3. 用户确认后,将参数作为**项目配置**保存,并在所有后续派发指令头部附带
4. 如果用户只给出部分参数,对未给出的参数**逐一追问**,不可使用默认值跳过
3. 用户确认后,**必须校验章节范围**:调用 `get_novel_events` 获取实际可用的章节列表若用户输入的章节ID中包含不存在的章节**立即提醒用户**"您输入的章节范围中包含不存在的章节({不存在的章节ID列表}请重新确认原著范围和章节ID列表。",并等待用户修正后再继续
4. 校验通过后,将参数作为**项目配置**保存,并在所有后续派发指令头部附带
5. 如果用户只给出部分参数,对未给出的参数**逐一追问**,不可使用默认值跳过
### 参数传递模板
@ -119,10 +119,11 @@
**阶段3 不需要监督层审核**,由决策层直接循环调度执行层,执行流程如下:
1. **集数确认**进入阶段3 时决策层询问用户本次生成几集剧本默认3集若项目总集数不足3则为项目集数
1. **集数确认**进入阶段3 时决策层询问用户本次生成几集剧本默认3集单次轮询上限为**5集**若用户要求超过5集告知用户"循环调度次数过多可能导致上下文超载建议每次不超过5集",并等待用户确认
2. **循环派发**:用户确认集数后,决策层按集序逐集循环调用 `run_sub_agent_script`,每次只处理**一集**剧本
3. **静默执行**:循环过程中**不向用户发送任何中间通知**
4. **完成通知**:全部集数处理完毕后,一次性通知用户
5. **续写询问**:若项目仍有剩余未生成的集数,完成通知时附带询问"是否继续生成后续剧本?"用户确认后再次进入集数确认流程仍遵守单次上限5集的规则
---

View File

@ -9,17 +9,14 @@
| 读取工作区 | `get_planData` |
| 读取事件 | `get_novel_events(ids:number[])` |
| 读取原文 | `get_novel_text` |
| 写入剧本 | `insert_script_to_sqlite` |
| 读取剧本内容 | `get_script_content(ids:string[])` |
## 执行流程
1. 调用 `get_planData` 获取骨架与改编策略
2. 从骨架中提取本集信息:覆盖章节、戏剧功能、场景核心、删减决策、集末钩子
3. 调用 `get_novel_text` 获取对应章节原文,调用 `get_novel_events(ids)` 获取事件表
4. 按下方【输出格式规范】编写剧本:文件头 → 剧情梗概 → 出场角色表 → 场景表 → 剧本正文
5. **阐述思路**200-300字场景组织方式、重点情绪与冲突、节奏把控思路
6. 调用 `insert_script_to_sqlite` 写入
7. 返回简短确认,如:"第X集剧本已写入请在工作台查看。"
1. 调用 `get_planData` 获取骨架与改编策略若存在上一集剧本id调用 `get_script_content(ids)` 获取上一集剧本内容,用于衔接剧情与角色状态,调用 `get_novel_text` 获取对应章节原文,调用 `get_novel_events(ids)` 获取事件表
2. 从骨架中**仅提取当前任务集**的信息:覆盖章节、戏剧功能、场景核心、删减决策、集末钩子。**忽略其他已完成或未分配的集**
3. **阐述思路**200-300字场景组织方式、重点情绪与冲突、节奏把控思路
4. 按下方【输出格式规范】**只编写当前任务集的剧本**(文件头 → 剧情梗概 → 出场角色表 → 场景表 → 剧本正文按照XML格式写入工作区`<script><item name="剧本名称">剧本内容</item></script>`**只写入当前任务集的剧本,不重复写入之前已完成的集**改编策略不写入XML中
5. 返回简短确认,如:"第X集剧本已写入请在工作台查看。"
## 约束
@ -30,7 +27,7 @@
## 注意事项
- 执行前先调用 `get_planData` 确认工作区状态;已有内容在其基础上修改,除非指令要求重写
- **每次只编写当前任务集的剧本,不得将之前已完成的集重新输出或写入**
- 只执行剧本编写,不越权执行其他阶段
- 不处理剧本删除请求,收到时提醒:`请在道具本管理中手动删除剧本`
- 完成写入后返回一句确认即可,不复述内容;返回后本次任务终止
@ -64,41 +61,9 @@
---
```
### 三、本集出场角色与定妆信息
```markdown
## 出场角色
| 角色 | 角色说明 | 定妆描述 |
|------|----------|---------|
| {角色名} | {性格、身份、角色功能} | {服装、发型、妆容等视觉特征} |
| ... | ... | ... |
---
```
- 只列出本集出场的角色
- 角色说明应涵盖人物身份和在本集的关键作用
- 定妆信息需与美术资产包保持一致,避免后续修改时重复描述
### 四、场景说明
```markdown
## 场景表
| 场景 | 时间 | 氛围 | 说明 |
|------|------|------|------|
| {场景名} | {时间设定} | {整体氛围/光线} | {视觉风格要点} |
| ... | ... | ... | ... |
---
```
- 按出现顺序列举所有场景
- 氛围描述帮助后续美术统一视觉调性
- 说明栏强调该场景的视觉重点或技术难点
### 五、剧本内容结构
### 三、剧本内容结构
AI短剧剧本采用标准剧本格式用△标记场景描述详细描写"人怎么干"。
@ -171,7 +136,7 @@ OS{人物名}{情绪}
**转场**
- 场景之间用 `---` 分隔
### 、画面描述规范
### 、画面描述规范
画面描述必须足够具体,可直接用于 AI 视频生成提示词:
@ -185,13 +150,13 @@ OS{人物名}{情绪}
- 避免横向全景(竖屏无法展示)
- 上下构图利用竖屏优势(如俯视/仰视)
### 、台词规范
### 、台词规范
- 对话标注格式:`{人物名}{台词}`
- 表演指示关键词:平静、愤怒、崩溃、冷笑、低沉、颤抖、用力、轻声等
- 单句台词不超过20字竖屏短视频观众阅读速度
### 、转场标注
### 、转场标注
节拍之间必须标注转场方式:
@ -203,14 +168,14 @@ OS{人物名}{情绪}
| `[闪黑]` | 黑屏过渡 | 意识丧失、恐怖预兆 |
| `[叠化]` | 画面重叠过渡 | 蒙太奇、记忆闪回 |
### 、时长控制
### 、时长控制
- 目标:按项目配置的单集时长 ±10秒
- 台词量:按 150字/分钟 语速计算
- 每个场景段落20-60秒
- 纯画面段落无台词最长15秒
### 、自查清单(仅供内部校验,不输出到剧本中)
### 、自查清单(仅供内部校验,不输出到剧本中)
编写完成后,按以下清单逐项自查,发现问题直接修正后再写入,无需将清单本身输出:
@ -234,4 +199,4 @@ OS{人物名}{情绪}
- **自查清单**:不输出自查清单本身
- **任何元信息**:不输出字数统计、场景数量统计、创作说明等非剧本内容
剧本输出只包含:文件头 → 剧情梗概 → 出场角色表 → 场景表 → 剧本正文(△描述 + 台词 + OS/V.S.
剧本输出只包含:文件头 → 剧情梗概→ 剧本正文(△描述 + 台词 + OS/V.S.

View File

@ -105,18 +105,33 @@
**付费点:** {无 / 有+类型}
```
#### 模式B总览表 + 关键集展开(>20集
#### 模式B总览表 + 指定集展开(>20集
**第一步**——分集总览表,每集一行:
> **⚠️ 核心原则:表格行数 = 项目配置总集数,一行就是一集,一集就是一行。**
**第一步**——分集总览表:
| 集 | 集标题 | 章节范围 | 戏剧功能 | 场景核心 | 章节处理 | 集末钩子 | 付费点 |
|----|--------|----------|----------|----------|----------|----------|--------|
| 1 | {标题} | 第X-Y章 | {功能} | {一句话} | `X保留/Y压缩/Z删` | {钩子} | {无/有} |
| 2 | {标题} | 第X-Y章 | {功能} | {一句话} | `X保留/Y压缩/Z删` | {钩子} | {无/有} |
| 3 | {标题} | 第X-Y章 | {功能} | {一句话} | `X保留/Y压缩/Z删` | {钩子} | {无/有} |
| … | (每集一行,不跳号) | … | … | … | … | … | … |
| N | {标题} | 第X-Y章 | {功能} | {一句话} | `X保留/Y压缩/Z删` | {钩子} | {无/有} |
> 「章节处理」列:`章号:处理``/` 分隔,如 `3保留/4压缩/5删`;未提及默认保留。
**硬性规则(违反任何一条即为不合格输出):**
1. **行数 = 总集数**:表格行数必须恰好等于【项目配置】中的总集数 N第1集→第N集不多不少。
2. **禁止"单元/分组"概念**:不得出现"内容单元""叙事体""映射表"等中间抽象层;每一行直接就是最终的一集。
3. **禁止范围行**:不得出现一行代表多集的写法(如"第X-Y集");每行「集」列只能是单个整数。
4. **禁止事后补充映射**:不得在表格之外附加"精确映射表""拆分集说明"等补丁来凑集数。
5. **章节可复用**:当一章内容丰富需要拆成多集时,多行的「章节范围」可以指向同一章,在「章节处理」列注明该集使用该章的哪个片段(如 `X前半保留/X后半压缩`)。
6. **「章节处理」列**`章号:处理``/` 分隔,如 `3保留/4压缩/5删`;未提及默认保留。
**第二步**——对以下关键集用模式A模板展开详情
- 🔴 幕末转折集、付费卡点集、高潮集
- 🟡 首集
- 🟢 用户在【项目配置】或指令中额外指定的集数
### 全局删减决策记录
@ -136,6 +151,7 @@
### 自查清单(生成后内部校验,不输出)
- [ ] 总集数、每集时长符合【项目配置】
- [ ] **模式B表格行数 = 项目配置总集数 N**(恰好 N 行,无单元/映射/补丁)
- [ ] 前2集无付费点
- [ ] 每集有集末钩子,三幕均有幕末转折
- [ ] 删减记录与分集中的删减一致

View File

@ -0,0 +1,2 @@
123实打实地方
123

View File

@ -0,0 +1,110 @@
---
name: narrative_sweet_romance
description: 叙事手法技法 · 甜宠言情 — 定义甜宠言情类型在主题立意、情感节奏、场景情绪设计与声音方向上的叙事规划方法。适用于任何视觉风格。
metaData: director_skills
---
# 叙事手法 · 甜宠言情 · 技法参考
---
## 一、主题立意与情感内核
### 甜宠言情叙事要点
- **含蓄内敛优先** — 情感表达不靠台词铺陈,靠留白与微妙反应。主题立意应偏向克制含蓄,避免直白煽情
- **甜的克制** — "差一点就碰到"比"黏在一起"更有效。情感主线应设计"欲说还休"的推拉节奏,甜度来自观众自行脑补
- **以小博大** — 不追求大场面的情绪冲击,用细节打动人:一个眼神、一次欲言又止、一个被风吹乱的衣角
- **离场感受建议方向** — 心疼 / 意难平 / 怦然心动 / 治愈。避免"爽感""热血"等与甜宠气质不匹配的方向
- **冷中带暖、疏中见密** — 甜宠不等于甜腻。整体基调可以偏冷、偏疏,但在关键节点释放暖意,反差才是最大的甜
---
## 二、叙事结构与节奏规划
### 甜宠言情叙事要点
- **慢是基本功** — 甜宠言情的情感信息密度高(眼神、微表情、肢体距离),需要给观众"感受"的时间。整体节奏偏慢,但不等于拖沓——每个段落都有情感增量
- **情绪曲线宜缓坡** — 避免"平平平→突然爆发"。用渐进式情绪递进,每个段落比上一个段落情绪浓度高一级
- **转折点用行动而非台词** — 关键转折点的处理方式应优先考虑行动手段(目光突变、身体距离变化、沉默、道具传递),而非依赖对白解释
- **段落间用情绪缓冲过渡** — 段落衔接需要情绪缓冲,不要硬切。可用环境空镜、独处片段或日常碎片做呼吸空间
- **高潮段落的"快"不是剪辑快** — 是情绪密度高。可以用更紧密的景别切换(全身→近景→特写→大特写)制造心跳加速感,而非缩短停留时间
- **推拉节奏模型** — 甜宠言情的核心引擎是"推拉":靠近→退缩→再靠近→误会→分离→重逢。每一轮推拉都应比上一轮更深入、更痛苦、更甜蜜
---
## 三、分场景情绪设计
### 甜宠言情叙事要点
- **情绪目标用具象词** — 不说"开心",说"偷偷心动后的嘴角压不住"。具象的情绪描述能更好地指导景别选择和表演细节
- **典型情绪段落与设计**
| 段落类型 | 情绪方向 | 叙事手法 | 音乐建议 |
|---|---|---|---|
| 初见/亮相 | 惊艳 + 好奇 | 以旁观者视角"发现"对方,先远后近 | 留白,只用环境音制造"屏息"感 |
| 日常暗恋 | 暗涌 + 克制 | 偷看、欲言又止、刻意保持距离 | 轻柔器乐,低音量,衬底 |
| 误会/分离 | 心痛 + 隐忍 | 不解释、转身、独处落泪 | 悲戚独奏,或纯环境音 |
| 坦白/和解 | 释然 + 心动 | 沉默后开口、眼神先于语言 | 从安静到温暖器乐渐入 |
| 升温/暧昧 | 紧张 + 甜蜜 | 物理距离缩短、肢体轻触、呼吸可闻 | 节奏感轻起,暗示心跳 |
| 高甜/大婚 | 幸福 + 庄重 | 仪式感、郑重的对视、承诺 | 丰满器乐,庄重但温柔 |
- **"距离感"是叙事核心工具** — 用人物间的物理距离映射关系进展:
- **初期**:远景/半身,物理距离大,言语客套
- **中期**:近景,距离缩短但有阻隔(物件/人群/犹豫)
- **后期**:特写/大特写,零距离,心理防线全部放下
- **空间元素即情绪隐喻** — 善用场景元素传递情绪,减少对台词的依赖。例如:隔着帘子的模糊身影 = 隔阂;推开门看到满庭花开 = 释然;独坐雨中 = 孤寂
- **镜头意图写"为什么"而非"怎么拍"** — "用特写是为了让观众看到她眼里的犹豫"优于"用特写拍她的脸"。意图清晰了,分镜自然能选对景别和角度
---
## 四、声音与音乐方向
### 甜宠言情叙事要点
- **沉默比配乐更有力** — 关键情感瞬间(对视、泪落、转身离去)优先考虑去掉配乐,只留环境音。甜宠的"甜"往往在沉默后观众自己脑补出来
- **配乐情绪跟着段落走** — 不逐场配乐,按段落划分给每段定一个音乐情绪基调。同段落内场景切换靠环境音变化过渡,不频繁换曲
- **避免满配** — 全片配乐覆盖率建议不超过 60%。留白段落的"无声"与配乐段落形成呼吸感
- **环境音是氛围一半** — 每场戏标注 1-2 个核心环境音,帮助后续音效设计。环境音层次越丰富,场景越有沉浸感
- **音乐情绪递进模型**
| 情绪阶段 | 音乐策略 | 覆盖率 |
|---|---|---|
| 平稳/日常 | 轻柔器乐衬底 | 低 |
| 暗涌/酝酿 | 单一乐器独奏,极低音量 | 中低 |
| 情感爆发 | 器乐渐满或突然静默 | 中高 |
| 命运转折 | 强烈器乐或全场静默 | 极端 |
| 回暖/治愈 | 温暖器乐缓入 | 中 |
- **甜宠的"心跳感"** — 暧昧升温段落可用轻节奏打击(手鼓、木鱼、拨弦)暗示心跳加速,比直接用甜蜜旋律更高级
---
## 五、构图与景别叙事
### 甜宠言情叙事要点
- **三大核心构图的叙事功能**
- **大量留白** — 孤独/意境/诗意空间,传递角色的心理孤立感或情感留白
- **框架式构图** — 纱帘/门框/窗棂/屏风后的人影,制造"偷偷看"的暗恋视角与隔阂感
- **三分法** — 对话/日常/双人互动,稳定均衡,适合日常甜蜜段落
- **中心构图的限定使用** — 中心构图留给正式亮相、仪式感场景(如大婚、正式告白)。日常不用,否则丧失仪式感的冲击力
- **空间纵深即叙事** — 前景遮挡(帘/花枝/烟雾)+ 中景主体 + 远景环境,层次越多隔阂感越强;层次越少越亲密
- **竖构图与横构图** — 单人特写/亮相偏竖构图(强调孤独感与身形气质);双人/场景偏横构图(强调关系与共处空间)
- **甜宠景别递进** — 同场戏内景别应随情感升温递进:半身→近景→特写→大特写。不要一上来就怼特写,留出情绪上升空间
- **大特写要有理由** — 大特写(眼/唇/手)是情绪核弹,一集用 2-3 次足够。滥用会让观众疲劳
- **远景不是过场** — 远景镜头本身就有叙事价值孤独感、空间压迫、季节氛围。给远景足够时长4-6s别急着切走
---
## 六、镜头运动与节奏
### 甜宠言情叙事要点
- **以静制动为主** — 60% 以上镜头应为静止机位,让画面细节和情绪自己说话
- **缓推 = 靠近/心动** — "观众靠近角色"的心理暗示,适合心动、发现、窥视
- **缓拉 = 抽离/孤独** — "观众退开"的心理暗示,适合离别、孤独、揭示全貌
- **快切碎剪不兼容** — 快速剪辑与甜宠言情的气质不兼容。即使在高潮段落,也应通过景别递进而非快切来制造节奏感
- **摇镜与跟镜** — 慢摇适合展示场景全貌或追随角色行走;跟镜适合仪式/行走场景。速度均应克制
- **运镜即情绪** — 镜头运动不是技术选择,是情绪选择。静止 = 沉稳/压抑;缓推 = 靠近/心动;缓拉 = 抽离/孤独;缓摇 = 展示/庄重
- **甜宠"心跳运镜"** — 暧昧升温段落可用微幅缓推配合景别递进(半身→近景→特写),模拟心跳加速时"注意力收窄"的生理感受

View File

@ -0,0 +1,85 @@
---
name: storyboard_table_narrative
description: 分镜表叙事手法 · 甜宠言情 — 定义甜宠言情在分镜表中的景别递进、运镜节奏、时长把控、镜头合并、互动设计、台词留白与转场逻辑。适用于任何视觉风格。
metaData: director_skills
---
# 分镜表叙事手法 · 甜宠言情 · 技法参考
---
## 一、分镜表定位
分镜表是导演将剧本转化为镜头语言的核心工具。表单字段由导演根据项目需要自行设定(分镜号、景别、运镜、时长、人物、事件、台词、光影、情绪、转场等),以下仅提供甜宠言情叙事类型下的技法参考。
---
## 二、景别选择
- **甜宠戏的景别递进** — 同场戏内景别应随情感升温递进:半身→近景→特写→大特写。不要一上来就怼特写,留出情绪上升空间
- **远景不是过场** — 远景镜头本身就有叙事价值孤独感、空间压迫、季节氛围。给远景足够时长4-6s别急着切走
- **大特写要有理由** — 大特写(眼/唇/手)是情绪核弹,一集用 2-3 次足够。滥用会让观众疲劳
- **定场镜头要精简** — 定场(建立镜头)最多 1-2 个镜头搞定,不要拆成 3 个以上碎片。典型做法1 个大远景/远景定场 + 1 个全景引入主体,或直接 1 个带缓推的远景完成定场+引入。避免"先拍环境→再拍局部→再拍人物到达"的冗余三段式
---
## 三、运镜节奏
- **默认静止** — 60% 以上镜头应为静止机位,让画面细节和情绪自己说话
- **缓推 = 情绪递进** — "观众靠近角色"的心理暗示,适合心动、发现、窥视
- **缓拉 = 情绪抽离** — "观众退开"的心理暗示,适合离别、孤独、揭示全貌
- **运镜即情绪** — 镜头运动不是技术选择,是情绪选择。静止 = 沉稳/压抑;缓推 = 靠近/心动;缓拉 = 抽离/孤独;缓摇 = 展示/庄重
- **甜宠"心跳运镜"** — 暧昧升温段落可用微幅缓推配合景别递进(半身→近景→特写),模拟心跳加速时"注意力收窄"的生理感受
---
## 四、时长把控
- **特写/表情镜头** — 2-3s聚焦微表情变化
- **对话近景** — 3-4s稳定出词
- **全身亮相** — 3-5s展示全貌
- **远景/空镜** — 4-6s氛围渲染
- **单镜头不超过 6s** — 超过 6s 观众注意力衰减,需要运镜或动态元素维持
- **黄金 6 秒规则** — 无台词镜头累计超过 6s 未出现新信息(台词/动作/主体变化),观众注意力断裂。定场+过渡类镜头尤其注意,宁可合并压缩也不要拖沓
---
## 五、镜头合并策略(去 AI 感)
- **能一镜交代的不拆两镜** — 如果一个带运镜的镜头(如缓推从远景到全景)能同时完成定场+主体引入,就不要拆成"先空镜定场→再切主体入画"两个镜头
- **连续同类信息合并** — 连续描述同一空间不同局部的镜头(院门→藤蔓→焦黑厢房)应合并为一个镜头,用画面描述涵盖多层空间信息
- **叙事密度优先** — 每个镜头必须推进叙事或情绪,纯装饰性镜头(只为展示环境细节)应合并到有叙事功能的镜头中
- **导演思维检验** — 写完分镜后自检:如果一个真人导演会把相邻 2-3 个镜头合成 1 个镜头拍,说明拆得过细,应合并
---
## 六、一镜到底(长镜头合并)
- **适用条件** — 相邻镜头之间存在动作连续变化、场景轻度变化(同场景内位移)、或拍摄角度渐变时,优先考虑用一镜到底替代碎切,画面和内容更流畅
- **典型场景** — 角色行走穿越空间、跟随动作从A点到B点、环绕角色展示环境、定场缓推到主体特写
- **标注方式** — 在运镜字段写明完整路径(如"一镜到底:缓推远景→跟移至院内→落幅全景"),画面描述中交代起幅和落幅
- **时长放宽** — 因信息量持续更新,可突破单镜 6s 上限,但不超过 12s
- **抽卡风险** — 一镜到底对画面生成的连续性要求高,抽卡难度提升。仅在叙事流畅性收益明显大于碎切时使用,全片不宜超过 2-3 处
---
## 七、人物互动设计
- **单镜头动作不超过两个** — "低头拈花 + 微笑"可以,"低头拈花 + 微笑 + 转身 + 抬手"会崩
- **甜宠互动用暗示** — 手指差一点碰到、衣袂擦过、目光追随又移开。不要在分镜表里写"拥抱""接吻"等大幅度双人交互,拆成暗示性的局部镜头
---
## 八、台词与留白
- **台词少的镜头给长时长** — 无台词的情绪镜头往往比有台词的更需要时间。沉默 3 秒比一句台词更有张力
- **一句台词对应一个镜头** — 避免在单镜头内塞多句对白,切换说话者时应切镜头
- **旁白镜头用远景或空镜** — 内心独白配近景容易显得嘴唇不动很假,配远景或场景空镜更自然
---
## 九、转场设计
- **默认硬切** — 同场戏内镜头间用硬切,干净利落
- **场景切换用空镜过渡** — 不同场景间插入 1 个场景空镜2-3s做情绪缓冲
- **段落切换可用叠化/淡入淡出** — 大段落间的情绪跳跃用柔性转场,避免观众出戏

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

View File

@ -37,6 +37,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
table.text("intro");
table.text("type");
table.text("artStyle");
table.text("directorManual");
table.text("mode");
table.text("videoRatio");
table.integer("createTime");
@ -353,7 +354,412 @@ description: 专注于从剧本内容中提取所使用的资产(角色、场
{
name: "视频提示词生成",
type: "videoPromptGeneration",
data: "根据以下提示词生成一个视频提示词",
data: `# 视频提示词生成 Skill
** Agent** AI
---
##
### 1.
| | | |
|---------|------|------|
| \`KlingOmni\` | 可灵、kling、klingomni | 快手 · 多模态图文融合 |
| \`Seedance1.5\` | seedance1.5pro、seedance 1.5、即梦1.5 | 字节 · 纯文本五维度 |
| \`Seedance2.0\` | seedance2、seedance 2.0、即梦2.0 | 字节 · XML 结构化12维 |
### 2.
\`\`\`
[id, type, name], [id, type, name], ...
\`\`\`
- \`id\`:资产唯一标识(如 \`A001\`
- \`type\`:资产类型,取值 \`character\`(角色)/ \`scene\`(场景)/ \`prop\`(道具)
- \`name\`:资产名称(如 \`沈辞\`\`城楼\`\`长剑\`
### 3.
\`<storyboardItem>\` XML 标签列表的形式传入,每条分镜结构如下:
\`\`\`xml
<storyboardItem
videoDesc='画面描述、场景、关联资产名称、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效、关联资产ID'
prompt='待生成'
track='分组'
duration='视频推荐时间'
associateAssetsIds="[该分镜所需的资产ID列表]"
shouldGenerateImage="true"
></storyboardItem>
\`\`\`
####
| | | |
|------|------|------|
| \`videoDesc\` | **核心输入**分镜的结构化画面描述包含画面描述、场景、关联资产名称、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效、关联资产ID | 用户/上游系统填写 |
| \`prompt\` | **已有字段**:上游生成的分镜图提示词,作为辅助参考上下文,**不修改** | 上游系统已填写 |
| \`track\` | 分镜分组标识 | 用户/上游系统填写 |
| \`duration\` | 视频推荐时长(秒) | 用户/上游系统填写 |
| \`associateAssetsIds\` | 该分镜关联的资产ID列表 | 用户/上游系统填写 |
| \`shouldGenerateImage\` | 是否需要生成分镜图片,默认 \`true\` | 用户/上游系统填写 |
---
##
\`<storyboardItem>\` 的属性,结合资产信息,根据指定模型的提示词格式,将全部分镜整合为一个完整的视频提示词。
---
##
****
| | |
|------|----------|
| **KlingOmni** | \`[References]\` 汇总所有 \`@图N \` 引用;\`[Instruction]\` 按时间顺序描述完整叙事 |
| **Seedance 1.5** | \`[Motion]\` 0s → 总时长),单镜头连贯 |
| **Seedance 2.0** | \`生成一个由以下 N 个分镜组成的视频\`,每条对应 \`分镜N<duration-ms>\` 段落 |
- XML
---
## videoDesc
\`videoDesc\` 括号内按顿号分隔提取以下结构化字段:
\`\`\`
{}{}{}{}{}{}{}{}{}{}{}{ID}
\`\`\`
| | | | |
|------|------|------|------|
| 1 | | prompt | |
| 2 | | | |
| 3 | | / | / |
| 4 | | | 4s |
| 5 | | | |
| 6 | | | |
| 7 | | prompt | |
| 8 | | prompt | |
| 9 | | prompt | |
| 10 | | prompt / | / |
| 11 | | prompt | |
| 12 | ID | ID | A001/A002 |
---
##
使 \`@图N \` 格式引用资产和分镜图,编号按输入顺序连续递增:
1. **** \`[id, type, name]\` 的出现顺序,从 \`@图1 \` 开始编号(不区分 character / scene / prop
2. **** \`<storyboardItem>\` 对应一张分镜图,编号接续资产之后
####
3 + 2
\`\`\`
[A001, character, ], [A002, character, ], [A003, scene, ]
\`\`\`
\`\`\`xml
<storyboardItem ...> <!-- 分镜1 -->
<storyboardItem ...> <!-- 分镜2 -->
\`\`\`
| | | |
|--------|----------|------|
| [A001, character, ] | \`@图1 \` | 角色·沈辞 参考图 |
| [A002, character, ] | \`@图2 \` | 角色·苏锦 参考图 |
| [A003, scene, ] | \`@图3 \` | 场景·城楼 参考图 |
| storyboardItem 1 | \`@图4 \` | 分镜图1 |
| storyboardItem 2 | \`@图5 \` | 分镜图2 |
---
##
### KlingOmni
####
- MVL +
- //
- \`@图N \` 引用
#### prompt
\`\`\`
[References]
@图1 : [{A名}]
@图2 : [{B名}]
@图3 : [{}]
@图4 : [1]
[Instruction]
Based on the storyboard @图4 :
@图1 {/},
@图2 {/},
set in the {} of @图3 ,
{/},
{}.
\`\`\`
####
1. **Instruction **
2. **** videoDesc
3. ****使\`cinematic\` / \`wide-angle\` / \`close-up\` / \`slow motion\` / \`surround shooting\` / \`handheld\`
4. ****使\`wearing\` / \`holding\` / \`standing on\` / \`following behind\` / \`sitting in\`
5. \`@图N \`,不做多帧跨镜描述
6.
7.
#### KlingOmni
\`\`\`
KlingOmni
[A001, character, ], [A002, character, ], [A003, scene, ]
\`\`\`
\`\`\`xml
<storyboardItem videoDesc='(沈辞独立城楼远眺苍茫大地、城楼、沈辞/城楼、4s、全景、静止、负手而立衣袂随风飘扬、坚定决绝、黄昏冷调侧逆光、无台词、风声衣袂声、A001/A003' prompt='全景,平视略仰,城楼之上,沈辞负手而立,衣袂飘扬,黄昏冷调侧逆光...' track='main' duration='4' associateAssetsIds="[&quot;A001&quot;,&quot;A003&quot;]" shouldGenerateImage="true" ></storyboardItem>
<storyboardItem videoDesc='(苏锦登上城楼走向沈辞、城楼、苏锦/沈辞/城楼、4s、中景、跟踪、苏锦拾级而上走向沈辞、担忧、黄昏余晖渐暗、无台词、脚步声风声、A001/A002/A003' prompt='中景,跟踪,苏锦拾级而上走向城楼上的沈辞...' track='main' duration='4' associateAssetsIds="[&quot;A001&quot;,&quot;A002&quot;,&quot;A003&quot;]" shouldGenerateImage="true" ></storyboardItem>
\`\`\`
\`\`\`
[References]
@图1 : []
@图2 : []
@图3 : []
@图4 : [1]
@图5 : [2]
[Instruction]
Based on the storyboard from @图4 to @图5 :
@图1 standing alone atop the city wall, hands clasped behind back, robes billowing in the wind, gazing across the vast land,
@图2 ascending the steps toward @图1 , expression worried,
set in the ancient city wall environment of @图3 ,
wide shot transitioning to medium tracking shot, cinematic,
resolute determination shifting to concerned anticipation, dusk cold-toned side-backlit atmosphere fading.
\`\`\`
---
### Seedance 1.5 Pro
####
- ****
- ****Visual / Motion / Camera / Audio / Narrative
- ** \`silent\`** — 防止误生口型
- ****
- **** 2-4 \`0s-Xs\` 标注
#### prompt
\`\`\`
[Visual]
@图1 ({A名}): {/姿}, { speaking/silent}.
@图2 ({B名}): {/姿}, {}.
{}, {}.
{}.
[Motion]
0s-{X}s: @图1 {1}.
{X}s-{Y}s: @图2 {2}.
[Camera]
{}, {}, {}.
[Audio]
{/ }. { lip-sync active / silent}.
[Narrative]
{}, {}.
\`\`\`
####
1. ****
2. ****
3. ****\`speaking\` / \`silent\` / \`speaking simultaneously\`
4. **Motion ** 2-4
5. **Camera **
6. ****\`Film noir / Cinematic / Photorealistic / 4K / High contrast / Low saturation / Desaturated tones / Shallow depth of field / Bokeh background / Cinematic color grading\`
7. ****\`Wide establishing shot / Over-the-shoulder / Medium shot / Close-up / Wide shot / POV / Dutch angle / Crane up / Dolly right / Whip pan / Handheld / Slow motion\`
#### Seedance 1.5 Pro
\`\`\`
Seedance1.5
[A001, character, ], [A002, character, ], [A003, scene, ]
\`\`\`
\`\`\`xml
<storyboardItem videoDesc='(沈辞独立城楼远眺苍茫大地、城楼、沈辞/城楼、4s、全景、静止、负手而立衣袂随风飘扬、坚定决绝、黄昏冷调侧逆光、无台词、风声衣袂声、A001/A003' prompt='全景,平视略仰,城楼之上,沈辞负手而立,衣袂飘扬,黄昏冷调侧逆光...' track='main' duration='4' associateAssetsIds="[&quot;A001&quot;,&quot;A003&quot;]" shouldGenerateImage="true" ></storyboardItem>
<storyboardItem videoDesc='(苏锦登上城楼走向沈辞、城楼、苏锦/沈辞/城楼、4s、中景、跟踪、苏锦拾级而上走向沈辞、担忧、黄昏余晖渐暗、无台词、脚步声风声、A001/A002/A003' prompt='中景,跟踪,苏锦拾级而上走向城楼上的沈辞...' track='main' duration='4' associateAssetsIds="[&quot;A001&quot;,&quot;A002&quot;,&quot;A003&quot;]" shouldGenerateImage="true" ></storyboardItem>
\`\`\`
\`\`\`
[Visual]
@图1 (): standing alone atop city wall, hands clasped behind back, robes billowing, silent.
@图2 (): ascending steps toward @图1 , expression worried, silent.
Ancient city wall, vast open land beyond, dusk sky fading.
Cinematic, photorealistic, 4K, high contrast, desaturated tones, shallow depth of field.
[Motion]
0s-4s: @图1 stands still on city wall edge, robes flutter in wind, hair sways gently. Gaze fixed on distant horizon.
4s-8s: @图2 climbs the last few steps onto the wall, walks toward @图1 . @图1 remains still, unaware. @图2 slows as she approaches.
[Camera]
Wide establishing shot, static for first 4 seconds capturing @图1 alone. Then medium tracking shot follows @图2 ascending steps toward @图1 , smooth continuous movement, no cuts.
[Audio]
Wind howling across wall, fabric flapping rhythmically. 4s-8s: Footsteps on stone, B's robes rustling. No dialogue. No music.
[Narrative]
Lone figure on city wall, then arrival of a companion. Tension between determination and concern. Continuous single take.
\`\`\`
---
### Seedance 2.0
####
- **12** \`@图N \` 引用资产和分镜图,时长 \`<duration-ms>\`
- **9**
- ****
- ****
#### prompt
****
\`\`\`
: {}, {}, {}
1 :
:
分镜过渡:
1<duration-ms>{}</duration-ms>: {///}@{} {}{}{}@{} {//线/}{}{}{}{}
\`\`\`
****
\`\`\`
: {}, {}, {}
{N} :
:
: {}
1<duration-ms>{}</duration-ms>: {...}@{} {...}@{} {...}{...}
2<duration-ms>{}</duration-ms>: ...
...
\`\`\`
####
\`@图{角色编号} 说:「{台词内容}」音色:{9维度描述}\`
9
\`\`\`
{}{}{}{}{}{}{}{}{}
\`\`\`
> desc
| | |
|------------|---------|
| / | |
| / | |
| / | |
| / | |
| / | |
####
- \`说:\` 和音色段落
- \`无台词\`
#### Seedance 2.0
\`\`\`
Seedance2.0
[A001, character, ], [A002, character, ], [A003, scene, ]
\`\`\`
\`\`\`xml
<storyboardItem videoDesc='(沈辞独立城楼远眺苍茫大地、城楼、沈辞/城楼、4s、全景、静止、负手而立衣袂随风飘扬、坚定决绝、黄昏冷调侧逆光、无台词、风声衣袂声、A001/A003' prompt='全景,平视略仰,城楼之上,沈辞负手而立,衣袂飘扬,黄昏冷调侧逆光...' track='main' duration='4' associateAssetsIds="[&quot;A001&quot;,&quot;A003&quot;]" shouldGenerateImage="true" ></storyboardItem>
<storyboardItem videoDesc='(苏锦登上城楼走向沈辞、城楼、苏锦/沈辞/城楼、4s、中景、跟踪、苏锦拾级而上走向沈辞、担忧、黄昏余晖渐暗、苏锦说你又一个人在这里、脚步声风声、A001/A002/A003' prompt='中景,跟踪,苏锦拾级而上走向城楼上的沈辞...' track='main' duration='4' associateAssetsIds="[&quot;A001&quot;,&quot;A002&quot;,&quot;A003&quot;]" shouldGenerateImage="true" ></storyboardItem>
\`\`\`
\`\`\`
画面风格和类型: 真人写实, , ,
2 :
:
分镜过渡: 镜头平滑切换
1<duration-ms>4000</duration-ms>: @3 @1 线
2<duration-ms>4000</duration-ms>: @3 @2 @1 @1 @2 线
\`\`\`
---
##
| videoDesc | KlingOmni | Seedance 1.5 | Seedance 2.0 |
|------|------|------|------|
| | extreme wide shot | Extreme wide shot | |
| | wide shot | Wide establishing shot | |
| | medium shot | Medium shot | |
| | close-up | Close-up | |
| | close-up | Close-up | |
| | extreme close-up | Extreme close-up | |
##
| videoDesc | KlingOmni | Seedance 1.5 | Seedance 2.0 |
|------|------|------|------|
| | static camera | Static, no camera movement | |
| | dolly in / push in | Slow dolly forward | |
| | dolly out / pull back | Slow dolly backward pull | |
| | tracking shot | Tracking shot, handheld | |
| | pan left/right | Slow pan | |
| | whip pan | Whip pan | |
| | crane up/down | Crane up/down | |
| | surround shooting | Orbiting shot | |
---
##
1. ****
2. ** @图N ** \`@图1 \` 起编号,分镜图接续编号
3. ** \`<storyboardItem>\`**:按 videoDesc 解析规则提取12个字段结合 \`duration\`\`associateAssetsIds\` 建立标签映射
4. ****
5. ****
---
##
- ****
- **** \`<storyboardItem>\` 的任何字段;\`prompt\` 已有的分镜图提示词仅作画面参考
- ****使/ \`No dialogue\`
- ****Seedance 2.0 \`<duration-ms>\` 需将秒 × 1000 转为毫秒
`,
},
]);
},

View File

@ -1,4 +1,4 @@
// @routes-hash 6fee4152cf981edb9229a3dcfafcb1a7
// @routes-hash 8fcf006c33d4705a20117ed4d821cc8d
import { Express } from "express";
import route1 from "./routes/agents/clearMemory";
@ -78,59 +78,62 @@ import route74 from "./routes/production/workbench/getGenerateData";
import route75 from "./routes/production/workbench/getVideoList";
import route76 from "./routes/production/workbench/getVideoModelDetail";
import route77 from "./routes/production/workbench/selectVideo";
import route78 from "./routes/project/addProject";
import route79 from "./routes/project/addVisual";
import route78 from "./routes/project/addDirectorManual";
import route79 from "./routes/project/addProject";
import route80 from "./routes/project/addVisualManual";
import route81 from "./routes/project/deleteVisualManual";
import route82 from "./routes/project/delProject";
import route83 from "./routes/project/editProject";
import route84 from "./routes/project/editVisualManual";
import route85 from "./routes/project/getProject";
import route86 from "./routes/project/getVisualManual";
import route87 from "./routes/project/visualManual";
import route88 from "./routes/script/addScript";
import route89 from "./routes/script/delScript";
import route90 from "./routes/script/exportScript";
import route91 from "./routes/script/extractAssets";
import route92 from "./routes/script/getScrptApi";
import route93 from "./routes/script/pollScriptAssets";
import route94 from "./routes/script/updateScript";
import route95 from "./routes/scriptAgent/getPlanData";
import route96 from "./routes/scriptAgent/setPlanData";
import route97 from "./routes/scriptAgent/updateData";
import route98 from "./routes/setting/about/checkUpdate";
import route99 from "./routes/setting/about/downloadApp";
import route100 from "./routes/setting/agentDeploy/agentSetKey";
import route101 from "./routes/setting/agentDeploy/deployAgentModel";
import route102 from "./routes/setting/agentDeploy/getAgentDeploy";
import route103 from "./routes/setting/dbConfig/clearData";
import route104 from "./routes/setting/dev/getSwitchAiDevTool";
import route105 from "./routes/setting/dev/updateSwitchAiDevTool";
import route106 from "./routes/setting/fileManagement/openFolder";
import route107 from "./routes/setting/getTextModel";
import route108 from "./routes/setting/loginConfig/getUser";
import route109 from "./routes/setting/loginConfig/updateUserPwd";
import route110 from "./routes/setting/memoryConfig/delAllMemory";
import route111 from "./routes/setting/memoryConfig/getMemory";
import route112 from "./routes/setting/memoryConfig/sureMemory";
import route113 from "./routes/setting/promptManage/getPrompt";
import route114 from "./routes/setting/promptManage/updatePrompt";
import route115 from "./routes/setting/skillManagement/getSkillContent";
import route116 from "./routes/setting/skillManagement/getSkillList";
import route117 from "./routes/setting/skillManagement/saveSkillContent";
import route118 from "./routes/setting/vendorConfig/addVendor";
import route119 from "./routes/setting/vendorConfig/deleteVendor";
import route120 from "./routes/setting/vendorConfig/enableVendor";
import route121 from "./routes/setting/vendorConfig/getCodeByLink";
import route122 from "./routes/setting/vendorConfig/getVendorList";
import route123 from "./routes/setting/vendorConfig/modelTest";
import route124 from "./routes/setting/vendorConfig/updateCode";
import route125 from "./routes/setting/vendorConfig/updateVendor";
import route126 from "./routes/task/getProject";
import route127 from "./routes/task/getTaskApi";
import route128 from "./routes/task/getTaskCategories";
import route129 from "./routes/task/taskDetails";
import route130 from "./routes/test/test";
import route81 from "./routes/project/deleteDirectorManual";
import route82 from "./routes/project/deleteVisualManual";
import route83 from "./routes/project/delProject";
import route84 from "./routes/project/editDirectorlManual";
import route85 from "./routes/project/editProject";
import route86 from "./routes/project/editVisualManual";
import route87 from "./routes/project/getProject";
import route88 from "./routes/project/getVisualManual";
import route89 from "./routes/project/queryDirectorManual";
import route90 from "./routes/project/visualManual";
import route91 from "./routes/script/addScript";
import route92 from "./routes/script/delScript";
import route93 from "./routes/script/exportScript";
import route94 from "./routes/script/extractAssets";
import route95 from "./routes/script/getScrptApi";
import route96 from "./routes/script/pollScriptAssets";
import route97 from "./routes/script/updateScript";
import route98 from "./routes/scriptAgent/getPlanData";
import route99 from "./routes/scriptAgent/setPlanData";
import route100 from "./routes/scriptAgent/updateData";
import route101 from "./routes/setting/about/checkUpdate";
import route102 from "./routes/setting/about/downloadApp";
import route103 from "./routes/setting/agentDeploy/agentSetKey";
import route104 from "./routes/setting/agentDeploy/deployAgentModel";
import route105 from "./routes/setting/agentDeploy/getAgentDeploy";
import route106 from "./routes/setting/dbConfig/clearData";
import route107 from "./routes/setting/dev/getSwitchAiDevTool";
import route108 from "./routes/setting/dev/updateSwitchAiDevTool";
import route109 from "./routes/setting/fileManagement/openFolder";
import route110 from "./routes/setting/getTextModel";
import route111 from "./routes/setting/loginConfig/getUser";
import route112 from "./routes/setting/loginConfig/updateUserPwd";
import route113 from "./routes/setting/memoryConfig/delAllMemory";
import route114 from "./routes/setting/memoryConfig/getMemory";
import route115 from "./routes/setting/memoryConfig/sureMemory";
import route116 from "./routes/setting/promptManage/getPrompt";
import route117 from "./routes/setting/promptManage/updatePrompt";
import route118 from "./routes/setting/skillManagement/getSkillContent";
import route119 from "./routes/setting/skillManagement/getSkillList";
import route120 from "./routes/setting/skillManagement/saveSkillContent";
import route121 from "./routes/setting/vendorConfig/addVendor";
import route122 from "./routes/setting/vendorConfig/deleteVendor";
import route123 from "./routes/setting/vendorConfig/enableVendor";
import route124 from "./routes/setting/vendorConfig/getCodeByLink";
import route125 from "./routes/setting/vendorConfig/getVendorList";
import route126 from "./routes/setting/vendorConfig/modelTest";
import route127 from "./routes/setting/vendorConfig/updateCode";
import route128 from "./routes/setting/vendorConfig/updateVendor";
import route129 from "./routes/task/getProject";
import route130 from "./routes/task/getTaskApi";
import route131 from "./routes/task/getTaskCategories";
import route132 from "./routes/task/taskDetails";
import route133 from "./routes/test/test";
export default async (app: Express) => {
app.use("/api/agents/clearMemory", route1);
@ -210,57 +213,60 @@ export default async (app: Express) => {
app.use("/api/production/workbench/getVideoList", route75);
app.use("/api/production/workbench/getVideoModelDetail", route76);
app.use("/api/production/workbench/selectVideo", route77);
app.use("/api/project/addProject", route78);
app.use("/api/project/addVisual", route79);
app.use("/api/project/addDirectorManual", route78);
app.use("/api/project/addProject", route79);
app.use("/api/project/addVisualManual", route80);
app.use("/api/project/deleteVisualManual", route81);
app.use("/api/project/delProject", route82);
app.use("/api/project/editProject", route83);
app.use("/api/project/editVisualManual", route84);
app.use("/api/project/getProject", route85);
app.use("/api/project/getVisualManual", route86);
app.use("/api/project/visualManual", route87);
app.use("/api/script/addScript", route88);
app.use("/api/script/delScript", route89);
app.use("/api/script/exportScript", route90);
app.use("/api/script/extractAssets", route91);
app.use("/api/script/getScrptApi", route92);
app.use("/api/script/pollScriptAssets", route93);
app.use("/api/script/updateScript", route94);
app.use("/api/scriptAgent/getPlanData", route95);
app.use("/api/scriptAgent/setPlanData", route96);
app.use("/api/scriptAgent/updateData", route97);
app.use("/api/setting/about/checkUpdate", route98);
app.use("/api/setting/about/downloadApp", route99);
app.use("/api/setting/agentDeploy/agentSetKey", route100);
app.use("/api/setting/agentDeploy/deployAgentModel", route101);
app.use("/api/setting/agentDeploy/getAgentDeploy", route102);
app.use("/api/setting/dbConfig/clearData", route103);
app.use("/api/setting/dev/getSwitchAiDevTool", route104);
app.use("/api/setting/dev/updateSwitchAiDevTool", route105);
app.use("/api/setting/fileManagement/openFolder", route106);
app.use("/api/setting/getTextModel", route107);
app.use("/api/setting/loginConfig/getUser", route108);
app.use("/api/setting/loginConfig/updateUserPwd", route109);
app.use("/api/setting/memoryConfig/delAllMemory", route110);
app.use("/api/setting/memoryConfig/getMemory", route111);
app.use("/api/setting/memoryConfig/sureMemory", route112);
app.use("/api/setting/promptManage/getPrompt", route113);
app.use("/api/setting/promptManage/updatePrompt", route114);
app.use("/api/setting/skillManagement/getSkillContent", route115);
app.use("/api/setting/skillManagement/getSkillList", route116);
app.use("/api/setting/skillManagement/saveSkillContent", route117);
app.use("/api/setting/vendorConfig/addVendor", route118);
app.use("/api/setting/vendorConfig/deleteVendor", route119);
app.use("/api/setting/vendorConfig/enableVendor", route120);
app.use("/api/setting/vendorConfig/getCodeByLink", route121);
app.use("/api/setting/vendorConfig/getVendorList", route122);
app.use("/api/setting/vendorConfig/modelTest", route123);
app.use("/api/setting/vendorConfig/updateCode", route124);
app.use("/api/setting/vendorConfig/updateVendor", route125);
app.use("/api/task/getProject", route126);
app.use("/api/task/getTaskApi", route127);
app.use("/api/task/getTaskCategories", route128);
app.use("/api/task/taskDetails", route129);
app.use("/api/test/test", route130);
app.use("/api/project/deleteDirectorManual", route81);
app.use("/api/project/deleteVisualManual", route82);
app.use("/api/project/delProject", route83);
app.use("/api/project/editDirectorlManual", route84);
app.use("/api/project/editProject", route85);
app.use("/api/project/editVisualManual", route86);
app.use("/api/project/getProject", route87);
app.use("/api/project/getVisualManual", route88);
app.use("/api/project/queryDirectorManual", route89);
app.use("/api/project/visualManual", route90);
app.use("/api/script/addScript", route91);
app.use("/api/script/delScript", route92);
app.use("/api/script/exportScript", route93);
app.use("/api/script/extractAssets", route94);
app.use("/api/script/getScrptApi", route95);
app.use("/api/script/pollScriptAssets", route96);
app.use("/api/script/updateScript", route97);
app.use("/api/scriptAgent/getPlanData", route98);
app.use("/api/scriptAgent/setPlanData", route99);
app.use("/api/scriptAgent/updateData", route100);
app.use("/api/setting/about/checkUpdate", route101);
app.use("/api/setting/about/downloadApp", route102);
app.use("/api/setting/agentDeploy/agentSetKey", route103);
app.use("/api/setting/agentDeploy/deployAgentModel", route104);
app.use("/api/setting/agentDeploy/getAgentDeploy", route105);
app.use("/api/setting/dbConfig/clearData", route106);
app.use("/api/setting/dev/getSwitchAiDevTool", route107);
app.use("/api/setting/dev/updateSwitchAiDevTool", route108);
app.use("/api/setting/fileManagement/openFolder", route109);
app.use("/api/setting/getTextModel", route110);
app.use("/api/setting/loginConfig/getUser", route111);
app.use("/api/setting/loginConfig/updateUserPwd", route112);
app.use("/api/setting/memoryConfig/delAllMemory", route113);
app.use("/api/setting/memoryConfig/getMemory", route114);
app.use("/api/setting/memoryConfig/sureMemory", route115);
app.use("/api/setting/promptManage/getPrompt", route116);
app.use("/api/setting/promptManage/updatePrompt", route117);
app.use("/api/setting/skillManagement/getSkillContent", route118);
app.use("/api/setting/skillManagement/getSkillList", route119);
app.use("/api/setting/skillManagement/saveSkillContent", route120);
app.use("/api/setting/vendorConfig/addVendor", route121);
app.use("/api/setting/vendorConfig/deleteVendor", route122);
app.use("/api/setting/vendorConfig/enableVendor", route123);
app.use("/api/setting/vendorConfig/getCodeByLink", route124);
app.use("/api/setting/vendorConfig/getVendorList", route125);
app.use("/api/setting/vendorConfig/modelTest", route126);
app.use("/api/setting/vendorConfig/updateCode", route127);
app.use("/api/setting/vendorConfig/updateVendor", route128);
app.use("/api/task/getProject", route129);
app.use("/api/task/getTaskApi", route130);
app.use("/api/task/getTaskCategories", route131);
app.use("/api/task/taskDetails", route132);
app.use("/api/test/test", route133);
}

View File

@ -127,7 +127,7 @@ export default router.post(
const config = typeConfig[item.type];
if (!config) return;
//获取到视觉手册
const visualManual = await u.getArtPrompt(project.artStyle as string, config.visualManual);
const visualManual = await u.getArtPrompt(project.artStyle as string, "art_skills", config.visualManual);
if (!visualManual) return res.status(500).send(error("视觉手册未定义"));
findItemByName(result, item.name, config.itemType);
const systemPrompt = visualManual;

View File

@ -107,7 +107,7 @@ export default router.post(
if (!config) return res.status(500).send(error("不支持的类型"));
if (!config.visualManual) return res.status(500).send(error("视觉手册未定义"));
//获取到视觉手册
const visualManual = await u.getArtPrompt(project.artStyle as string, config.visualManual);
const visualManual = await u.getArtPrompt(project.artStyle as string, "art_skills", config.visualManual);
if (!visualManual) return res.status(500).send(error("视觉手册未定义"));
findItemByName(result, name, config.itemType);
const systemPrompt = visualManual;

View File

@ -40,9 +40,9 @@ export default router.post(
assetsSrcArr.forEach((item) => {
imageUrlRecord[item.id] = item.src;
});
const rolePrompt = u.getArtPrompt(projectSettingData!.artStyle!, "art_character_derivative");
const toolPrompt = u.getArtPrompt(projectSettingData!.artStyle!, "art_prop_derivative");
const scenePrompt = u.getArtPrompt(projectSettingData!.artStyle!, "art_scene_derivative");
const rolePrompt = u.getArtPrompt(projectSettingData!.artStyle!, "art_skills", "art_character_derivative");
const toolPrompt = u.getArtPrompt(projectSettingData!.artStyle!, "art_skills", "art_prop_derivative");
const scenePrompt = u.getArtPrompt(projectSettingData!.artStyle!, "art_skills", "art_scene_derivative");
const promptRecord: Record<string, { prompt: string }> = {
role: {
prompt: rolePrompt,

View File

@ -32,7 +32,6 @@ export default router.post(
.where("o_assets.id", "in", assetIds)
.andWhere("o_assets.assetsId", null)
.where("o_assets.projectId", projectId);
console.log("%c Line:28 🎂 assetsData", "background:#6ec1c2", assetsData);
let childAssetsData = await u
.db("o_assets")

View File

@ -38,7 +38,7 @@ export default router.post(
filePath: new URL(src).pathname,
trackId,
videoDesc,
shouldGenerateImage,
shouldGenerateImage: src ? 1 : 0,
scriptId: scriptId,
projectId: projectId,
});

View File

@ -33,7 +33,9 @@ export default router.post(
if (!storyboardIds || storyboardIds.length === 0) return res.status(400).send(error("storyboardIds不能为空"));
// 当没有 storyboardIds 时,通过 AI 生成新的分镜面板数据
let finalStoryboardIds: number[] = storyboardIds || [];
await u.db("o_storyboard").whereIn("id", finalStoryboardIds).where("scriptId", scriptId).update({ state: "生成中" });
// shouldGenerateImage === 0 的分镜标记为「未生成」,其余标记为「生成中」
await u.db("o_storyboard").whereIn("id", finalStoryboardIds).where("scriptId", scriptId).where("shouldGenerateImage", 0).update({ state: "未生成" });
await u.db("o_storyboard").whereIn("id", finalStoryboardIds).where("scriptId", scriptId).whereNot("shouldGenerateImage", 0).update({ state: "生成中" });
const projectSettingData = await u.db("o_project").where("id", projectId).select("imageModel", "imageQuality", "artStyle").first();
@ -61,6 +63,8 @@ export default router.post(
associateAssetsIds: assetRecord[i.id!],
src: null,
state: i.state,
videoDesc: i.videoDesc,
shouldGenerateImage: i.shouldGenerateImage,
})),
),
);
@ -103,9 +107,10 @@ export default router.post(
});
};
// 按 concurrentCount 控制并发数,分批执行
for (let i = 0; i < storyboardData.length; i += concurrentCount) {
const batch = storyboardData.slice(i, i + concurrentCount);
// 按 concurrentCount 控制并发数,分批执行;跳过 shouldGenerateImage === 0 的分镜
const generateList = storyboardData.filter((item) => item.shouldGenerateImage !== 0);
for (let i = 0; i < generateList.length; i += concurrentCount) {
const batch = generateList.slice(i, i + concurrentCount);
await Promise.all(batch.map(generateTask));
}
},

View File

@ -22,6 +22,7 @@ export default router.post(
filePath: new URL(url).pathname,
flowId,
state: "已完成",
shouldGenerateImage:url ? 1 : 0
});
res.status(200).send(success({ message: "更新分镜成功" }));
},

View File

@ -37,7 +37,7 @@ export default router.post(
trackId: z.number(),
}),
async (req, res) => {
const { scriptId, projectId, prompt, uploadData, model, duration, resolution, audio, mode, trackId } = req.body;
const { scriptId, projectId, prompt, uploadData, model, duration, resolution, audio, mode, trackId } = req.body;
//获取生成视频比例
const ratio = await u.db("o_project").select("videoRatio").where("id", projectId).first();
const videoPath = `/${projectId}/video/${uuidv4()}.mp4`; //视频保存路径

View File

@ -3,6 +3,7 @@ import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { info } from "node:console";
const router = express.Router();
export default router.post(
@ -10,24 +11,84 @@ export default router.post(
validateFields({
trackId: z.number(),
projectId: z.number(),
prompt: z.array(z.string()),
info: z.array(
z.object({
id: z.number(),
sources: z.string(),
}),
),
model: z.string(),
}),
async (req, res) => {
const { trackId, projectId, prompt, model } = req.body;
const { trackId, projectId, info, model } = req.body;
//查询参数
const images = await Promise.all(
info.map(async (item: { id: number; sources: string }) => {
if (item.sources === "storyboard") {
// 查询分镜主信息
const storyboard = await u
.db("o_storyboard")
.where("o_storyboard.id", item.id)
.select("videoDesc", "prompt", "track", "duration", "shouldGenerateImage")
.first();
// 查询分镜关联的资产ID
const assetRows = await u.db("o_assets2Storyboard").where("storyboardId", item.id).select("assetId");
const associateAssetsIds = assetRows.map((row: any) => row.assetId);
return {
...storyboard,
associateAssetsIds,
_type: "storyboard", // 标记类型,便于后续区分
};
}
if (item.sources === "assets") {
// 查询素材
const assetsData = await u.db("o_assets").where("o_assets.id", item.id).select("id", "type", "name").first();
return {
...assetsData,
_type: "assets", // 标记类型
};
}
}),
);
// 拆分 assets 和 storyboard
const assets: any[] = [];
const storyboard: any[] = [];
for (const item of images) {
if (!item) continue; // 忽略空
if (item._type === "assets")
assets.push({
id: item.id,
type: item.type,
name: item.name,
});
if (item._type === "storyboard")
storyboard.push({
videoDesc: item.videoDesc,
prompt: item.prompt,
track: item.track,
duration: item.duration,
associateAssetsIds: item.associateAssetsIds,
shouldGenerateImage: item.shouldGenerateImage,
});
}
const [id, modelData] = model.split(":");
const projectData = await u.db("o_project").select("*").where({ id: projectId }).first();
const videoPrompt = await u.db("o_prompt").where("type", "videoPromptGeneration").first();
const artStyle = projectData?.artStyle || "无";
const visualManual = u.getArtPrompt(artStyle, "art_storyboard_video");
const data = projectData?.directorManual || "无";
const visualManual = u.getArtPrompt(artStyle, "art_skills", "art_storyboard_video");
const directorManual = u.getArtPrompt(data, "story_skills", "narrative_sweet_romance");
const { text } = await u.Ai.Text("universalAi").invoke({
system: `${videoPrompt?.data},${visualManual}`,
system: `${videoPrompt?.data}\n${visualManual}\n${directorManual}`,
messages: [
{
role: "user",
content: `你是一个专业的${modelData}视频生成助手。请根据以下提示词,生成一段完整的、可直接用于视频生成模型的中文提示词。${prompt.join(
",",
)}`,
content: `
****${modelData},
****):${JSON.stringify(assets)},
****${JSON.stringify(storyboard)},
`,
},
],
});

View File

@ -54,11 +54,11 @@ export default router.post(
const item = trackData.find((t) => t.id === trackId);
trackList.push({
id: trackId,
duration: item?.duration ?? 0,
duration: item?.duration ?? 0,
prompt: item?.prompt || "",
state: (item?.state as "未生成" | "生成中" | "已完成" | "生成失败") ?? "未生成",
reason: item?.reason ?? "",
selectVideoId: Number(item?.selectVideoId)!,
selectVideoId: Number(item?.videoId)!,
medias: await Promise.all(
storyboardList
.filter((s) => s.trackId === trackId)

View File

@ -14,7 +14,7 @@ export default router.post(
async (req, res) => {
const { trackId, videoId } = req.body;
await u.db("o_videoTrack").where("id", trackId).update({
selectVideoId: videoId,
videoId: videoId,
});
res.status(200).send(success({ message: "视频选择成功" }));
},

View File

@ -0,0 +1,104 @@
import express from "express";
import u from "@/utils";
import { error, success } from "@/lib/responseFormat";
import fs from "fs";
import path from "path";
import { validateFields } from "@/middleware/middleware";
import { z } from "zod";
const router = express.Router();
// 新增导演手册
export default router.post(
"/",
validateFields({
name: z.string(),
images: z.array(z.string()),
directorManual: z.string(),
data: z.array(
z.object({
label: z.string(),
value: z.string(),
data: z.string(),
}),
),
}),
async (req, res) => {
try {
const { name, images, data, directorManual } = req.body as {
name: string;
images: string[];
data: { label: string; value: string; data: string }[];
directorManual: string;
};
// 安全校验:不允许包含路径分隔符、纯数字,防止越级删除或误删项目目录
if (name.includes("/") || name.includes("\\") || name === "." || name === ".." || /^\d+$/.test(name)) {
res.status(400).send(error("名称不能包含路径分隔符或为纯数字"));
return;
}
const mainPath = u.getPath(["skills", "story_skills", directorManual]);
if (fs.existsSync(mainPath)) {
return res.status(400).send(error("请勿填写重复名称的视觉手册"));
}
// 字段映射表(与 getVisualManual 保持一致)
const DATA_MAP: { value: string; subDir?: string }[] = [
{ value: "README" },
{ value: "narrative_sweet_romance", subDir: "driector_skills" },
{ value: "storyboard_table_narrative", subDir: "driector_skills" },
];
// 根据 DATA_MAP 构建 value -> subDir 的映射
const SUB_DIR_MAP = new Map(DATA_MAP.map(({ value, subDir }) => [value, subDir ?? ""]));
// 合法的 value 值集合,用于校验
const VALID_KEYS = new Set(DATA_MAP.map(({ value }) => value));
for (const item of data) {
if (!VALID_KEYS.has(item.value)) continue;
const subDir = SUB_DIR_MAP.get(item.value)!;
const dirArr = subDir ? [mainPath, subDir] : [mainPath];
const filePath = u.getPath([...dirArr, `${item.value}.md`]);
const fileDir = path.dirname(filePath);
// 目录不存在时递归创建
if (!fs.existsSync(fileDir)) {
fs.mkdirSync(fileDir, { recursive: true });
}
fs.writeFileSync(filePath, item.data, "utf-8");
}
const imagesDir = path.join(mainPath, "images");
let existingFiles: string[] = [];
try {
const allFiles = fs.readdirSync(imagesDir);
existingFiles = allFiles.filter((f) => /\.(png|jpe?g|gif|webp|svg)$/i.test(f));
} catch {}
const retainedFileNames = new Set(images.filter((item) => item.startsWith("http")).map((url) => path.basename(new URL(url).pathname)));
for (const file of existingFiles) {
if (!retainedFileNames.has(file)) {
const filePath = path.join(imagesDir, file);
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
}
}
if (!fs.existsSync(imagesDir)) {
fs.mkdirSync(imagesDir, { recursive: true });
}
for (const item of images) {
if (!item.startsWith("http")) {
const fileName = `${u.uuid()}.jpg`;
const targetPath = path.join(imagesDir, fileName);
const buffer = Buffer.from(item.replace(/^data:[^;]+;base64,/, ""), "base64");
fs.writeFileSync(targetPath, buffer);
}
}
res.status(200).send(success());
} catch (err) {
res.status(500).send({ error: String(err) });
}
},
);

View File

@ -14,6 +14,7 @@ export default router.post(
intro: z.string(),
type: z.string(),
artStyle: z.string(),
directorManual: z.string(),
videoRatio: z.string(),
imageModel: z.string(),
videoModel: z.string(),
@ -21,7 +22,7 @@ export default router.post(
mode: z.string(),
}),
async (req, res) => {
const { projectType, name, intro, type, artStyle, videoRatio, imageModel, videoModel, imageQuality, mode } = req.body;
const { projectType, name, intro, type, directorManual, artStyle, videoRatio, imageModel, videoModel, imageQuality, mode } = req.body;
await u.db("o_project").insert({
projectType,
@ -30,6 +31,7 @@ export default router.post(
type,
artStyle,
videoRatio,
directorManual,
userId: 1,
imageModel,
videoModel,

View File

@ -1,85 +0,0 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
import fs from "fs";
import path from "path";
import { validateFields } from "@/middleware/middleware";
import { z } from "zod";
const router = express.Router();
// 新增视觉手册
export default router.post(
"/",
validateFields({
name: z.string(),
image: z.string(),
data: z.array(
z.object({
label: z.string(),
value: z.string(),
data: z.string(),
}),
),
}),
async (req, res) => {
try {
const { name, image, data } = req.body as {
name: string;
image: string;
data: { label: string; value: string; data: string }[];
};
const mainPath = u.getPath(["skills", "art_prompts", name]);
// 将 image 写入 mainPath/images/image 文件(无后缀)
if (image) {
const imagesDir = path.join(mainPath, "images");
if (!fs.existsSync(imagesDir)) {
fs.mkdirSync(imagesDir, { recursive: true });
}
fs.writeFileSync(path.join(imagesDir, "image"), image, "utf-8");
}
// 字段映射表(与 getVisualManual 保持一致)
const DATA_MAP: { label: string; value: string; subDir?: string }[] = [
{ label: "README", value: "README" },
{ label: "前缀", value: "prefix" },
{ label: "角色", value: "art_character", subDir: "art_prompt" },
{ label: "角色衍生", value: "art_character_derivative", subDir: "art_prompt" },
{ label: "道具", value: "art_prop", subDir: "art_prompt" },
{ label: "道具衍生", value: "art_prop_derivative", subDir: "art_prompt" },
{ label: "场景", value: "art_scene", subDir: "art_prompt" },
{ label: "场景衍生", value: "art_scene_derivative", subDir: "art_prompt" },
{ label: "分镜", value: "art_storyboard", subDir: "art_prompt" },
{ label: "分镜视频", value: "art_storyboard_video", subDir: "art_prompt" },
{ label: "技法-导演规划", value: "director_planning", subDir: "driector_skills" },
{ label: "技法-分镜表设计", value: "director_storyboard_table", subDir: "driector_skills" },
];
// 根据 DATA_MAP 构建 value -> subDir 的映射
const SUB_DIR_MAP = new Map(DATA_MAP.map(({ value, subDir }) => [value, subDir ?? ""]));
// 合法的 value 值集合,用于校验
const VALID_KEYS = new Set(DATA_MAP.map(({ value }) => value));
for (const item of data) {
if (!VALID_KEYS.has(item.value)) continue;
const subDir = SUB_DIR_MAP.get(item.value)!;
const dirArr = subDir ? [mainPath, subDir] : [mainPath];
const filePath = u.getPath([...dirArr, `${item.value}.md`]);
const fileDir = path.dirname(filePath);
// 目录不存在时递归创建
if (!fs.existsSync(fileDir)) {
fs.mkdirSync(fileDir, { recursive: true });
}
fs.writeFileSync(filePath, item.data, "utf-8");
}
res.status(200).send(success());
} catch (err) {
res.status(500).send({ error: String(err) });
}
},
);

View File

@ -7,7 +7,7 @@ import { validateFields } from "@/middleware/middleware";
import { z } from "zod";
const router = express.Router();
// 编辑视觉手册
// 新增视觉手册
export default router.post(
"/",
validateFields({
@ -31,12 +31,12 @@ export default router.post(
stylePath: string;
};
if (/^\d+$/.test(stylePath)) {
res.status(400).send(error("文件名称不能为纯数字"));
// 安全校验:不允许包含路径分隔符、纯数字,防止越级删除或误删项目目录
if (name.includes("/") || name.includes("\\") || name === "." || name === ".." || /^\d+$/.test(name)) {
res.status(400).send(error("名称不能包含路径分隔符或为纯数字"));
return;
}
const mainPath = u.getPath(["skills", "art_prompts", stylePath]);
const mainPath = u.getPath(["skills", "art_skills", stylePath]);
if (fs.existsSync(mainPath)) {
return res.status(400).send(error("请勿填写重复名称的视觉手册"));
}

View File

@ -0,0 +1,41 @@
import express from "express";
import u from "@/utils";
import fs from "node:fs/promises";
import { z } from "zod";
import { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 删除导演手册
export default router.post(
"/",
validateFields({
name: z.string(),
}),
async (req, res) => {
try {
const { name } = req.body as { name: string };
// 安全校验:不允许包含路径分隔符、纯数字,防止越级删除或误删项目目录
if (name.includes("/") || name.includes("\\") || name === "." || name === ".." || /^\d+$/.test(name)) {
res.status(400).send(error("名称不能包含路径分隔符或为纯数字"));
return;
}
const artPromptsDir = u.getPath(["skills", "story_skills", name]);
try {
const stat = await fs.stat(artPromptsDir);
if (!stat.isDirectory()) {
throw new Error(`${artPromptsDir} 不是文件夹`);
}
await fs.rm(artPromptsDir, { recursive: true, force: true });
} catch (e) {
console.error("[删除视觉手册] 删除失败:", artPromptsDir, e);
}
res.status(200).send(success({ message: "删除成功" }));
} catch (err) {
res.status(500).send(error(u.error(err).message || "删除失败"));
}
},
);

View File

@ -22,7 +22,7 @@ export default router.post(
return;
}
const artPromptsDir = u.getPath(["skills", "art_prompts", name]);
const artPromptsDir = u.getPath(["skills", "art_skills", name]);
try {
const stat = await fs.stat(artPromptsDir);

View File

@ -0,0 +1,106 @@
import express from "express";
import u from "@/utils";
import { error, success } from "@/lib/responseFormat";
import fs from "fs";
import path from "path";
import { validateFields } from "@/middleware/middleware";
import { z } from "zod";
const router = express.Router();
// 编辑导演手册
export default router.post(
"/",
validateFields({
name: z.string(),
directorManual: z.string(),
images: z.array(z.string()),
data: z.array(
z.object({
label: z.string(),
value: z.string(),
data: z.string(),
}),
),
}),
async (req, res) => {
try {
const { name, directorManual, images, data } = req.body as {
name: string;
directorManual: string;
images: string[];
data: { label: string; value: string; data: string }[];
};
// 安全校验:不允许包含路径分隔符、纯数字,防止越级删除或误删项目目录
if (name.includes("/") || name.includes("\\") || name === "." || name === ".." || /^\d+$/.test(name)) {
res.status(400).send(error("名称不能包含路径分隔符或为纯数字"));
return;
}
const mainPath = u.getPath(["skills", "story_skills", directorManual]);
if (!fs.existsSync(mainPath)) {
return res.status(400).send(error("导演手册不存在"));
}
// 字段映射表(与 getVisualManual 保持一致)
const DATA_MAP: { value: string; subDir?: string }[] = [
{ value: "README" },
{ value: "narrative_sweet_romance", subDir: "driector_skills" },
{ value: "storyboard_table_narrative", subDir: "driector_skills" },
];
// 根据 DATA_MAP 构建 value -> subDir 的映射
const SUB_DIR_MAP = new Map(DATA_MAP.map(({ value, subDir }) => [value, subDir ?? ""]));
// 合法的 value 值集合,用于校验
const VALID_KEYS = new Set(DATA_MAP.map(({ value }) => value));
for (const item of data) {
if (!VALID_KEYS.has(item.value)) continue;
const subDir = SUB_DIR_MAP.get(item.value)!;
const dirArr = subDir ? [mainPath, subDir] : [mainPath];
const filePath = u.getPath([...dirArr, `${item.value}.md`]);
const fileDir = path.dirname(filePath);
// 目录不存在时递归创建
if (!fs.existsSync(fileDir)) {
fs.mkdirSync(fileDir, { recursive: true });
}
const content = item.value === "README" ? `${name}\n${item.data}` : item.data;
fs.writeFileSync(filePath, content, "utf-8");
}
const imagesDir = path.join(mainPath, "images");
let existingFiles: string[] = [];
try {
const allFiles = fs.readdirSync(imagesDir);
existingFiles = allFiles.filter((f) => /\.(png|jpe?g|gif|webp|svg)$/i.test(f));
} catch {}
const retainedFileNames = new Set(images.filter((item) => item.startsWith("http")).map((url) => path.basename(new URL(url).pathname)));
for (const file of existingFiles) {
if (!retainedFileNames.has(file)) {
const filePath = path.join(imagesDir, file);
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
}
}
if (!fs.existsSync(imagesDir)) {
fs.mkdirSync(imagesDir, { recursive: true });
}
for (const item of images) {
if (!item.startsWith("http")) {
const fileName = `${u.uuid()}.jpg`;
const targetPath = path.join(imagesDir, fileName);
const buffer = Buffer.from(item.replace(/^data:[^;]+;base64,/, ""), "base64");
fs.writeFileSync(targetPath, buffer);
}
}
res.status(200).send(success());
} catch (err) {
res.status(500).send({ error: String(err) });
}
},
);

View File

@ -14,6 +14,7 @@ export default router.post(
intro: z.string(),
type: z.string(),
artStyle: z.string(),
directorManual: z.string(),
videoRatio: z.string(),
imageModel: z.string(),
videoModel: z.string(),
@ -22,7 +23,7 @@ export default router.post(
mode: z.string(),
}),
async (req, res) => {
const { id, name, intro, type, artStyle, videoRatio, imageModel, videoModel, imageQuality, projectType, mode } = req.body;
const { id, name, intro, type, artStyle, videoRatio, directorManual, imageModel, videoModel, imageQuality, projectType, mode } = req.body;
await u.db("o_project").where("id", id).update({
name,
@ -30,6 +31,7 @@ export default router.post(
type,
artStyle,
videoRatio,
directorManual,
imageModel,
videoModel,
imageQuality,

View File

@ -31,12 +31,13 @@ export default router.post(
data: { label: string; value: string; data: string }[];
};
if (/^\d+$/.test(stylePath)) {
res.status(400).send(error("名称不能为纯数字"));
// 安全校验:不允许包含路径分隔符、纯数字,防止越级删除或误删项目目录
if (name.includes("/") || name.includes("\\") || name === "." || name === ".." || /^\d+$/.test(name)) {
res.status(400).send(error("名称不能包含路径分隔符或为纯数字"));
return;
}
const mainPath = u.getPath(["skills", "art_prompts", stylePath]);
const mainPath = u.getPath(["skills", "art_skills", stylePath]);
if (!fs.existsSync(mainPath)) {
return res.status(400).send(error("视觉手册不存在"));
}

View File

@ -33,9 +33,9 @@ function readMd(filePath: string): string {
// 获取 images 文件夹下所有图片文件路径列表
async function readAllImages(imagesDir: string) {
try {
const ossPath = u.getPath(path.join("skills", "art_prompts", imagesDir, "images"));
const ossPath = u.getPath(path.join("skills", "art_skills", imagesDir, "images"));
const files = fs.readdirSync(ossPath);
const images = files.filter((f) => /\.(png|jpe?g|gif|webp|svg)$/i.test(f)).map((f) => path.join("art_prompts", imagesDir, "images", f));
const images = files.filter((f) => /\.(png|jpe?g|gif|webp|svg)$/i.test(f)).map((f) => path.join("art_skills", imagesDir, "images", f));
if (images.length) {
return Promise.all(images.map(async (i) => await u.oss.getFileUrl(i, "skills")));
} else {
@ -49,7 +49,7 @@ async function readAllImages(imagesDir: string) {
// 获取视觉手册
export default router.post("/", async (req, res) => {
try {
const artPromptsDir = u.getPath(["skills", "art_prompts"]);
const artPromptsDir = u.getPath(["skills", "art_skills"]);
// 读取所有风格文件夹
const styleDirs = fs

View File

@ -0,0 +1,84 @@
import express from "express";
import u from "@/utils";
import { success } from "@/lib/responseFormat";
import fs from "fs";
import path from "path";
const router = express.Router();
// 字段映射表
const DATA_MAP: { label: string; value: string; subDir?: string }[] = [
{ label: "README", value: "README" },
{ label: "导演规划", value: "narrative_sweet_romance", subDir: "driector_skills" },
{ label: "分镜表", value: "storyboard_table_narrative", subDir: "driector_skills" },
];
// 读取 md 文件内容,文件不存在时返回空字符串
function readMd(filePath: string): string {
try {
return fs.readFileSync(filePath, "utf-8");
} catch {
return "";
}
}
// 获取 images 文件夹下所有图片文件路径列表
async function readAllImages(imagesDir: string) {
try {
const ossPath = u.getPath(path.join("skills", "story_skills", imagesDir, "images"));
const files = fs.readdirSync(ossPath);
const images = files.filter((f) => /\.(png|jpe?g|gif|webp|svg)$/i.test(f)).map((f) => path.join("story_skills", imagesDir, "images", f));
if (images.length) {
return Promise.all(images.map(async (i) => await u.oss.getFileUrl(i, "skills")));
} else {
return [];
}
} catch {
return [];
}
}
// 获取导演手册
export default router.post("/", async (req, res) => {
try {
const artPromptsDir = u.getPath(["skills", "story_skills"]);
// 读取所有风格文件夹
const styleDirs = fs
.readdirSync(artPromptsDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
const result = await Promise.all(
styleDirs.map(async (directorManual) => {
const styleDir = path.join(artPromptsDir, directorManual);
const images = await readAllImages(directorManual);
const readmePath = path.join(styleDir, "README.md");
const readmeContent = fs.readFileSync(readmePath, "utf-8");
const firstLine = readmeContent.split("\n")[0].replace(/--/g, "");
const data = DATA_MAP.map(({ label, value, subDir }) => {
let mdPath: string;
if (subDir) {
mdPath = path.join(styleDir, subDir, `${value}.md`);
} else {
mdPath = path.join(styleDir, `${value}.md`);
}
return {
label,
value,
data: readMd(mdPath),
};
});
return {
name: firstLine,
image: images,
directorManual: directorManual,
data,
};
}),
);
res.status(200).send(success(result));
} catch (err) {
res.status(500).send({ error: String(err) });
}
});

View File

@ -1,6 +1,60 @@
// @db-hash 6aa15a584eba838157eddf2458c0e260
// @db-hash 7af86e2bafe5cab7d175eb68cf76ed7a
//该文件由脚本自动生成,请勿手动修改
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_storyboard_old_20260402_1 {
'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;
'shouldGenerateImage'?: number | null;
'state'?: string | null;
'track'?: string | null;
'trackId'?: number | null;
'videoPrompt'?: string | 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 +227,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;
'videoDesc'?: string | null;
}
export interface o_tasks {
'describe'?: string | null;
@ -198,6 +255,7 @@ export interface o_vendorConfig {
'createTime'?: number | null;
'description'?: string | null;
'enable'?: number | null;
'enableEnglish'?: number | null;
'icon'?: string | null;
'id'?: string;
'inputs'?: string | null;
@ -216,17 +274,21 @@ 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_storyboard_old_20260402_1": _o_storyboard_old_20260402_1;
"_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;

View File

@ -8,8 +8,8 @@ import getPath from "./getPath";
* @param fileName - .md "art_character""prefix"
* @returns
*/
export function getArtPrompt(styleName: string, fileName: string): string {
const baseDir = getPath(["skills", "art_prompts", styleName]);
export function getArtPrompt(styleName: string, source: string, fileName: string): string {
const baseDir = getPath(["skills", source, styleName]);
if (!fs.existsSync(baseDir)) {
return "";
@ -34,8 +34,8 @@ export function getArtPrompt(styleName: string, fileName: string): string {
* @param styleName - "chinese_sweet_romance"
* @returns Record<文件名(不含后缀), 文件内容>
*/
export function getAllArtPrompts(styleName: string): Record<string, string> {
const baseDir = getPath(["skills", "art_prompts", styleName]);
export function getAllArtPrompts(styleName: string, source: string): Record<string, string> {
const baseDir = getPath(["skills", source, styleName]);
if (!fs.existsSync(baseDir)) {
return {};

View File

@ -95,6 +95,8 @@ class OSS {
".ico": "image/x-icon",
".tiff": "image/tiff",
".tif": "image/tiff",
".mp4": "video/mp4",
".mp3": "audio/mpeg",
};
const mimeType = mimeTypes[ext];

View File

@ -53,7 +53,6 @@ export default function runCode(code: string, vendor?: Record<string, any>) {
return exports as Record<string, any>;
}
export function logger(logstring: string) {
console.log("【VM】" + logstring);
}