diff --git a/data/skills/art_prompts/chinese_sweet_romance/README.md b/data/skills/art_skills/antiquity/README.md similarity index 88% rename from data/skills/art_prompts/chinese_sweet_romance/README.md rename to data/skills/art_skills/antiquity/README.md index 7c3c2c7..c597d72 100644 --- a/data/skills/art_prompts/chinese_sweet_romance/README.md +++ b/data/skills/art_skills/antiquity/README.md @@ -1,3 +1,6 @@ +123水电费水电费 +123 +123 123 1212121212的王师傅水电费第三方水电费 1212121212 diff --git a/data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_character.md b/data/skills/art_skills/antiquity/art_prompt/art_character.md similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_character.md rename to data/skills/art_skills/antiquity/art_prompt/art_character.md diff --git a/data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_character_derivative.md b/data/skills/art_skills/antiquity/art_prompt/art_character_derivative.md similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_character_derivative.md rename to data/skills/art_skills/antiquity/art_prompt/art_character_derivative.md diff --git a/data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_prop.md b/data/skills/art_skills/antiquity/art_prompt/art_prop.md similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_prop.md rename to data/skills/art_skills/antiquity/art_prompt/art_prop.md diff --git a/data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_prop_derivative.md b/data/skills/art_skills/antiquity/art_prompt/art_prop_derivative.md similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_prop_derivative.md rename to data/skills/art_skills/antiquity/art_prompt/art_prop_derivative.md diff --git a/data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_scene.md b/data/skills/art_skills/antiquity/art_prompt/art_scene.md similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_scene.md rename to data/skills/art_skills/antiquity/art_prompt/art_scene.md diff --git a/data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_scene_derivative.md b/data/skills/art_skills/antiquity/art_prompt/art_scene_derivative.md similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_scene_derivative.md rename to data/skills/art_skills/antiquity/art_prompt/art_scene_derivative.md diff --git a/data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_storyboard.md b/data/skills/art_skills/antiquity/art_prompt/art_storyboard.md similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_storyboard.md rename to data/skills/art_skills/antiquity/art_prompt/art_storyboard.md diff --git a/data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_storyboard_video.md b/data/skills/art_skills/antiquity/art_prompt/art_storyboard_video.md similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/art_prompt/art_storyboard_video.md rename to data/skills/art_skills/antiquity/art_prompt/art_storyboard_video.md diff --git a/data/skills/art_prompts/chinese_sweet_romance/driector_skills/director_planning.md b/data/skills/art_skills/antiquity/driector_skills/director_planning.md similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/driector_skills/director_planning.md rename to data/skills/art_skills/antiquity/driector_skills/director_planning.md diff --git a/data/skills/art_prompts/chinese_sweet_romance/driector_skills/director_storyboard_table.md b/data/skills/art_skills/antiquity/driector_skills/director_storyboard_table.md similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/driector_skills/director_storyboard_table.md rename to data/skills/art_skills/antiquity/driector_skills/director_storyboard_table.md diff --git a/data/skills/art_prompts/chinese_sweet_romance/images/2b1cb9c6-54f9-4e19-bf15-b546c80147bc.jpg b/data/skills/art_skills/antiquity/images/2b1cb9c6-54f9-4e19-bf15-b546c80147bc.jpg similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/images/2b1cb9c6-54f9-4e19-bf15-b546c80147bc.jpg rename to data/skills/art_skills/antiquity/images/2b1cb9c6-54f9-4e19-bf15-b546c80147bc.jpg diff --git a/data/skills/art_prompts/chinese_sweet_romance/images/b5bd58ee-c304-477f-9c18-b40f814a5901.jpg b/data/skills/art_skills/antiquity/images/b5bd58ee-c304-477f-9c18-b40f814a5901.jpg similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/images/b5bd58ee-c304-477f-9c18-b40f814a5901.jpg rename to data/skills/art_skills/antiquity/images/b5bd58ee-c304-477f-9c18-b40f814a5901.jpg diff --git a/data/skills/art_prompts/chinese_sweet_romance/images/e4b20248-54f1-4e63-ab2d-cb72cd1886b8.jpg b/data/skills/art_skills/antiquity/images/e4b20248-54f1-4e63-ab2d-cb72cd1886b8.jpg similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/images/e4b20248-54f1-4e63-ab2d-cb72cd1886b8.jpg rename to data/skills/art_skills/antiquity/images/e4b20248-54f1-4e63-ab2d-cb72cd1886b8.jpg diff --git a/data/skills/art_prompts/chinese_sweet_romance/images/f0f89ce4-b197-4bd6-9794-3d14d1c3ab1e.jpg b/data/skills/art_skills/antiquity/images/f0f89ce4-b197-4bd6-9794-3d14d1c3ab1e.jpg similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/images/f0f89ce4-b197-4bd6-9794-3d14d1c3ab1e.jpg rename to data/skills/art_skills/antiquity/images/f0f89ce4-b197-4bd6-9794-3d14d1c3ab1e.jpg diff --git a/data/skills/art_prompts/chinese_sweet_romance/prefix.md b/data/skills/art_skills/antiquity/prefix.md similarity index 100% rename from data/skills/art_prompts/chinese_sweet_romance/prefix.md rename to data/skills/art_skills/antiquity/prefix.md diff --git a/data/skills/production_agent_decision.md b/data/skills/production_agent_decision.md index 2640cb4..d346195 100644 --- a/data/skills/production_agent_decision.md +++ b/data/skills/production_agent_decision.md @@ -114,11 +114,20 @@ | 审核 | 不需要 | **决策层行为:** -向执行层派发阶段5分镜面板写入任务,收到确认后进入阶段6。 + +阶段4完成后、派发阶段5之前,根据模型参数 `多参` 决定写入模式: + +| 模型参数 `多参` | 决策层操作 | +|----------------|-----------| +| 是 | 向用户询问:使用 **"纯文本多参模式"** 还是 **"分镜图辅助多参模式"**,等待用户确认后,将所选模式随任务指令一起派发给执行层 | +| 否 | 无需询问用户,直接以 **"首位帧模式"** 派发给执行层 | + +收到执行层完成确认后进入阶段6。 **阶段特有约束:** - 必须严格依据阶段4分镜表逐行写入,行数与时长保持一致 - 分组累计时长不得超过 15 秒 +- 派发执行层时必须在指令中明确携带写入模式(纯文本多参模式 / 分镜图辅助多参模式 / 首位帧模式) --- diff --git a/data/skills/production_agent_execution.md b/data/skills/production_agent_execution.md index 7522a1e..851f71d 100644 --- a/data/skills/production_agent_execution.md +++ b/data/skills/production_agent_execution.md @@ -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` 的分镜数据行逐行写入分镜面板(排除表头与分隔行), -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` 的分镜数据行逐行写入分镜面板(排除表头与分隔行),根据模式差异化输出: + - **纯文本多参模式**:`` + - **分镜图辅助多参模式**:`` + - **首位帧模式**:`` +8. 写入完成后,仅返回一句确认:`已完成分镜面板写入({当前模式名称})` ### 分镜提示词 · 通用基础技法 @@ -664,14 +684,23 @@ Image [2]: @图2 — [外貌关键描述] ### 约束 - 前置条件:分镜表已构建完成且用户已确认 -- 你必须使用XML格式写入工作区分镜面板: -- 分组总时长约束:每个 `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) | **必填** | **必填** | +| 提示词技法加载 | 跳过 | 加载通用基础技法 + 风格专属技法 | 加载通用基础技法 + 风格专属技法 | + --- ## 六、分镜图生成 diff --git a/data/skills/production_agent_supervision.md b/data/skills/production_agent_supervision.md index dfe6349..5e99ae2 100644 --- a/data/skills/production_agent_supervision.md +++ b/data/skills/production_agent_supervision.md @@ -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 + #### 剧本覆盖度(严重) 验证方法: diff --git a/data/skills/script_agent_decision.md b/data/skills/script_agent_decision.md index 43fb03a..8782cb1 100644 --- a/data/skills/script_agent_decision.md +++ b/data/skills/script_agent_decision.md @@ -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集的规则) --- diff --git a/data/skills/script_execution_script.md b/data/skills/script_execution_script.md index 4a2f29a..40d8430 100644 --- a/data/skills/script_execution_script.md +++ b/data/skills/script_execution_script.md @@ -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格式写入工作区``,**只写入当前任务集的剧本,不重复写入之前已完成的集**,改编策略不写入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.) \ No newline at end of file +剧本输出只包含:文件头 → 剧情梗概→ 剧本正文(△描述 + 台词 + OS/V.S.) \ No newline at end of file diff --git a/data/skills/script_execution_skeleton.md b/data/skills/script_execution_skeleton.md index 6752087..03a4755 100644 --- a/data/skills/script_execution_skeleton.md +++ b/data/skills/script_execution_skeleton.md @@ -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集无付费点 - [ ] 每集有集末钩子,三幕均有幕末转折 - [ ] 删减记录与分集中的删减一致 diff --git a/data/skills/story_skills/sweetPet/README.md b/data/skills/story_skills/sweetPet/README.md new file mode 100644 index 0000000..307d654 --- /dev/null +++ b/data/skills/story_skills/sweetPet/README.md @@ -0,0 +1,2 @@ +123实打实地方 +123 \ No newline at end of file diff --git a/data/skills/story_skills/sweetPet/driector_skills/narrative_sweet_romance.md b/data/skills/story_skills/sweetPet/driector_skills/narrative_sweet_romance.md new file mode 100644 index 0000000..be6e764 --- /dev/null +++ b/data/skills/story_skills/sweetPet/driector_skills/narrative_sweet_romance.md @@ -0,0 +1,110 @@ +--- +name: narrative_sweet_romance +description: 叙事手法技法 · 甜宠言情 — 定义甜宠言情类型在主题立意、情感节奏、场景情绪设计与声音方向上的叙事规划方法。适用于任何视觉风格。 +metaData: director_skills +--- + +# 叙事手法 · 甜宠言情 · 技法参考 + +--- + +## 一、主题立意与情感内核 + +### 甜宠言情叙事要点 + +- **含蓄内敛优先** — 情感表达不靠台词铺陈,靠留白与微妙反应。主题立意应偏向克制含蓄,避免直白煽情 +- **甜的克制** — "差一点就碰到"比"黏在一起"更有效。情感主线应设计"欲说还休"的推拉节奏,甜度来自观众自行脑补 +- **以小博大** — 不追求大场面的情绪冲击,用细节打动人:一个眼神、一次欲言又止、一个被风吹乱的衣角 +- **离场感受建议方向** — 心疼 / 意难平 / 怦然心动 / 治愈。避免"爽感""热血"等与甜宠气质不匹配的方向 +- **冷中带暖、疏中见密** — 甜宠不等于甜腻。整体基调可以偏冷、偏疏,但在关键节点释放暖意,反差才是最大的甜 + +--- + +## 二、叙事结构与节奏规划 + +### 甜宠言情叙事要点 + +- **慢是基本功** — 甜宠言情的情感信息密度高(眼神、微表情、肢体距离),需要给观众"感受"的时间。整体节奏偏慢,但不等于拖沓——每个段落都有情感增量 +- **情绪曲线宜缓坡** — 避免"平平平→突然爆发"。用渐进式情绪递进,每个段落比上一个段落情绪浓度高一级 +- **转折点用行动而非台词** — 关键转折点的处理方式应优先考虑行动手段(目光突变、身体距离变化、沉默、道具传递),而非依赖对白解释 +- **段落间用情绪缓冲过渡** — 段落衔接需要情绪缓冲,不要硬切。可用环境空镜、独处片段或日常碎片做呼吸空间 +- **高潮段落的"快"不是剪辑快** — 是情绪密度高。可以用更紧密的景别切换(全身→近景→特写→大特写)制造心跳加速感,而非缩短停留时间 +- **推拉节奏模型** — 甜宠言情的核心引擎是"推拉":靠近→退缩→再靠近→误会→分离→重逢。每一轮推拉都应比上一轮更深入、更痛苦、更甜蜜 + +--- + +## 三、分场景情绪设计 + +### 甜宠言情叙事要点 + +- **情绪目标用具象词** — 不说"开心",说"偷偷心动后的嘴角压不住"。具象的情绪描述能更好地指导景别选择和表演细节 +- **典型情绪段落与设计** — + +| 段落类型 | 情绪方向 | 叙事手法 | 音乐建议 | +|---|---|---|---| +| 初见/亮相 | 惊艳 + 好奇 | 以旁观者视角"发现"对方,先远后近 | 留白,只用环境音制造"屏息"感 | +| 日常暗恋 | 暗涌 + 克制 | 偷看、欲言又止、刻意保持距离 | 轻柔器乐,低音量,衬底 | +| 误会/分离 | 心痛 + 隐忍 | 不解释、转身、独处落泪 | 悲戚独奏,或纯环境音 | +| 坦白/和解 | 释然 + 心动 | 沉默后开口、眼神先于语言 | 从安静到温暖器乐渐入 | +| 升温/暧昧 | 紧张 + 甜蜜 | 物理距离缩短、肢体轻触、呼吸可闻 | 节奏感轻起,暗示心跳 | +| 高甜/大婚 | 幸福 + 庄重 | 仪式感、郑重的对视、承诺 | 丰满器乐,庄重但温柔 | + +- **"距离感"是叙事核心工具** — 用人物间的物理距离映射关系进展: + - **初期**:远景/半身,物理距离大,言语客套 + - **中期**:近景,距离缩短但有阻隔(物件/人群/犹豫) + - **后期**:特写/大特写,零距离,心理防线全部放下 +- **空间元素即情绪隐喻** — 善用场景元素传递情绪,减少对台词的依赖。例如:隔着帘子的模糊身影 = 隔阂;推开门看到满庭花开 = 释然;独坐雨中 = 孤寂 +- **镜头意图写"为什么"而非"怎么拍"** — "用特写是为了让观众看到她眼里的犹豫"优于"用特写拍她的脸"。意图清晰了,分镜自然能选对景别和角度 + +--- + +## 四、声音与音乐方向 + +### 甜宠言情叙事要点 + +- **沉默比配乐更有力** — 关键情感瞬间(对视、泪落、转身离去)优先考虑去掉配乐,只留环境音。甜宠的"甜"往往在沉默后观众自己脑补出来 +- **配乐情绪跟着段落走** — 不逐场配乐,按段落划分给每段定一个音乐情绪基调。同段落内场景切换靠环境音变化过渡,不频繁换曲 +- **避免满配** — 全片配乐覆盖率建议不超过 60%。留白段落的"无声"与配乐段落形成呼吸感 +- **环境音是氛围一半** — 每场戏标注 1-2 个核心环境音,帮助后续音效设计。环境音层次越丰富,场景越有沉浸感 +- **音乐情绪递进模型** — + +| 情绪阶段 | 音乐策略 | 覆盖率 | +|---|---|---| +| 平稳/日常 | 轻柔器乐衬底 | 低 | +| 暗涌/酝酿 | 单一乐器独奏,极低音量 | 中低 | +| 情感爆发 | 器乐渐满或突然静默 | 中高 | +| 命运转折 | 强烈器乐或全场静默 | 极端 | +| 回暖/治愈 | 温暖器乐缓入 | 中 | + +- **甜宠的"心跳感"** — 暧昧升温段落可用轻节奏打击(手鼓、木鱼、拨弦)暗示心跳加速,比直接用甜蜜旋律更高级 + +--- + +## 五、构图与景别叙事 + +### 甜宠言情叙事要点 + +- **三大核心构图的叙事功能** — + - **大量留白** — 孤独/意境/诗意空间,传递角色的心理孤立感或情感留白 + - **框架式构图** — 纱帘/门框/窗棂/屏风后的人影,制造"偷偷看"的暗恋视角与隔阂感 + - **三分法** — 对话/日常/双人互动,稳定均衡,适合日常甜蜜段落 +- **中心构图的限定使用** — 中心构图留给正式亮相、仪式感场景(如大婚、正式告白)。日常不用,否则丧失仪式感的冲击力 +- **空间纵深即叙事** — 前景遮挡(帘/花枝/烟雾)+ 中景主体 + 远景环境,层次越多隔阂感越强;层次越少越亲密 +- **竖构图与横构图** — 单人特写/亮相偏竖构图(强调孤独感与身形气质);双人/场景偏横构图(强调关系与共处空间) +- **甜宠景别递进** — 同场戏内景别应随情感升温递进:半身→近景→特写→大特写。不要一上来就怼特写,留出情绪上升空间 +- **大特写要有理由** — 大特写(眼/唇/手)是情绪核弹,一集用 2-3 次足够。滥用会让观众疲劳 +- **远景不是过场** — 远景镜头本身就有叙事价值(孤独感、空间压迫、季节氛围)。给远景足够时长(4-6s),别急着切走 + +--- + +## 六、镜头运动与节奏 + +### 甜宠言情叙事要点 + +- **以静制动为主** — 60% 以上镜头应为静止机位,让画面细节和情绪自己说话 +- **缓推 = 靠近/心动** — "观众靠近角色"的心理暗示,适合心动、发现、窥视 +- **缓拉 = 抽离/孤独** — "观众退开"的心理暗示,适合离别、孤独、揭示全貌 +- **快切碎剪不兼容** — 快速剪辑与甜宠言情的气质不兼容。即使在高潮段落,也应通过景别递进而非快切来制造节奏感 +- **摇镜与跟镜** — 慢摇适合展示场景全貌或追随角色行走;跟镜适合仪式/行走场景。速度均应克制 +- **运镜即情绪** — 镜头运动不是技术选择,是情绪选择。静止 = 沉稳/压抑;缓推 = 靠近/心动;缓拉 = 抽离/孤独;缓摇 = 展示/庄重 +- **甜宠"心跳运镜"** — 暧昧升温段落可用微幅缓推配合景别递进(半身→近景→特写),模拟心跳加速时"注意力收窄"的生理感受 diff --git a/data/skills/story_skills/sweetPet/driector_skills/storyboard_table_narrative.md b/data/skills/story_skills/sweetPet/driector_skills/storyboard_table_narrative.md new file mode 100644 index 0000000..e768f6e --- /dev/null +++ b/data/skills/story_skills/sweetPet/driector_skills/storyboard_table_narrative.md @@ -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)做情绪缓冲 +- **段落切换可用叠化/淡入淡出** — 大段落间的情绪跳跃用柔性转场,避免观众出戏 diff --git a/data/skills/story_skills/sweetPet/images/ed2fcc56-0069-4666-beea-6b50d9648896.jpg b/data/skills/story_skills/sweetPet/images/ed2fcc56-0069-4666-beea-6b50d9648896.jpg new file mode 100644 index 0000000..1d0cd85 Binary files /dev/null and b/data/skills/story_skills/sweetPet/images/ed2fcc56-0069-4666-beea-6b50d9648896.jpg differ diff --git a/src/lib/initDB.ts b/src/lib/initDB.ts index d1c44d3..b9587e4 100644 --- a/src/lib/initDB.ts +++ b/src/lib/initDB.ts @@ -37,6 +37,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => 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. 分镜信息 + +分镜以 \`\` XML 标签列表的形式传入,每条分镜结构如下: + +\`\`\`xml + +\`\`\` + +#### 输入字段说明 + +| 属性 | 说明 | 来源 | +|------|------|------| +| \`videoDesc\` | **核心输入**:分镜的结构化画面描述,包含画面描述、场景、关联资产名称、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效、关联资产ID | 用户/上游系统填写 | +| \`prompt\` | **已有字段**:上游生成的分镜图提示词,作为辅助参考上下文,**不修改** | 上游系统已填写 | +| \`track\` | 分镜分组标识 | 用户/上游系统填写 | +| \`duration\` | 视频推荐时长(秒) | 用户/上游系统填写 | +| \`associateAssetsIds\` | 该分镜关联的资产ID列表 | 用户/上游系统填写 | +| \`shouldGenerateImage\` | 是否需要生成分镜图片,默认 \`true\` | 用户/上游系统填写 | + +--- + +## 任务目标 + +读取所有 \`\` 的属性,结合资产信息,根据指定模型的提示词格式,将全部分镜整合为一个完整的视频提示词。 + +--- + +## 输出格式 + +将所有分镜整合为**一个完整的视频提示词**输出(非逐条独立): + +| 模型 | 整合方式 | +|------|----------| +| **KlingOmni** | \`[References]\` 汇总所有 \`@图N \` 引用;\`[Instruction]\` 按时间顺序描述完整叙事 | +| **Seedance 1.5** | 五维度按时间轴连续编排(\`[Motion]\` 0s → 总时长),单镜头连贯 | +| **Seedance 2.0** | \`生成一个由以下 N 个分镜组成的视频\`,每条对应 \`分镜N\` 段落 | + +- 仅输出视频提示词文本,不输出 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. **分镜图**:每条 \`\` 对应一张分镜图,编号接续资产之后 + +#### 示例 + +输入 3 个资产 + 2 条分镜: +\`\`\` +资产信息[A001, character, 沈辞], [A002, character, 苏锦], [A003, scene, 城楼] +\`\`\` +\`\`\`xml + + +\`\`\` + +编号结果: + +| 输入项 | 引用标签 | 说明 | +|--------|----------|------| +| [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 + + +\`\`\` + +输出: +\`\`\` +[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 + + +\`\`\` + +输出: +\`\`\` +[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 \` 引用资产和分镜图,时长 \`\` +- **音色参数9维度精细描述**(有台词时必填) +- **毫秒级时长控制** +- **中文提示词** + +#### prompt 生成模板 + +**单分镜模板:** +\`\`\` +画面风格和类型: {风格}, {色调}, {类型} + +生成一个由以下 1 个分镜组成的视频: + +场景: +分镜过渡: 无 + +分镜1{毫秒数}: 时间:{日/夜/晨/黄昏},场景图片:@图{场景编号} ,镜头:{景别},{角度},{运镜},@图{角色编号} {动作/表情/视线朝向/站位描述}。{台词与音色描述(如有)}。{背景环境补充}。{光影氛围}。{运镜补充}。 +\`\`\` + +**多分镜模板:** +\`\`\` +画面风格和类型: {风格}, {色调}, {类型} + +生成一个由以下 {N} 个分镜组成的视频: + +场景: +分镜过渡: {全局过渡描述} + +分镜1{毫秒数}: 时间:{...},场景图片:@图{场景编号} ,镜头:{...},@图{角色编号} {...}。{...}。 +分镜2{毫秒数}: ... +... +\`\`\` + +#### 音色生成规则(有台词时必填) + +台词格式:\`@图{角色编号} 说:「{台词内容}」音色:{9维度描述}\` + +9维度按顺序填写: +\`\`\` +{性别},{年龄音色},{音调},{音色质感},{声音厚度},{发音方式},{气息},{语速},{特殊质感} +\`\`\` + +> 当 desc 中未明确音色信息时,根据角色类型从以下参考表推断: + +| 角色类型特征 | 默认音色 | +|------------|---------| +| 男性权威/霸气角色 | 男声,中年音色,音调低沉,音色浑厚有力,声音厚重,发音标准,气息极其沉稳,语速偏慢 | +| 女性温柔/甜美角色 | 女声,青年音色,音调中等偏高,音色质感明亮清脆,声音清亮柔和,气息充沛平稳,带温婉真诚感 | +| 男性年轻/普通角色 | 男声,青年音色,音调中等,音色干净,声音厚度适中,发音清晰,气息平稳,语速适中 | +| 女性活泼/外向角色 | 女声,青年音色,音调偏高,音色清脆活泼,声音轻盈,气息充沛,语速偏快,带笑意和感染力 | +| 反派/冷酷角色 | 男声,中年音色,音调低沉,音色质感干燥偏暗,声音带沙砾感,气息平稳,语速极慢,有威胁感 | + +#### 无台词分镜处理 +- 不写 \`说:\` 和音色段落 +- 在动作描述后标注 \`无台词\` + +#### Seedance 2.0 完整示例 + +输入: +\`\`\` +模型:Seedance2.0 +资产信息[A001, character, 沈辞], [A002, character, 苏锦], [A003, scene, 城楼] +\`\`\` +\`\`\`xml + + +\`\`\` + +输出: +\`\`\` +画面风格和类型: 真人写实, 电影风格, 冷调, 古风 + +生成一个由以下 2 个分镜组成的视频: + +场景: +分镜过渡: 镜头平滑切换,从全景过渡到中景跟踪,焦点从沈辞独处转向苏锦到来。 + +分镜14000: 时间:黄昏,场景图片:@图3 ,镜头:全景,平视略仰,静止镜头,@图1 独立城楼之上,负手而立,衣袂随风飘扬,目光远眺苍茫大地,神情肃然面容沉着,眼神坚定目光清冽,眉眼沉静气质凛然。无台词。背景是古城楼砖石纹理清晰,远方大地苍茫辽阔,天际线冷暖交替。黄昏斜射余晖侧逆光,冷调为主,长影拉伸,轮廓光微勾勒人物边缘,光感诗意。镜头静止。 + +分镜24000: 时间:黄昏,场景图片:@图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. **逐条解析 \`\`**:按 videoDesc 解析规则提取12个字段,结合 \`duration\`、\`associateAssetsIds\` 建立标签映射 +4. **整合为一个完整的视频提示词**:按目标模型格式编排全部分镜 +5. **输出视频提示词** + +--- + +## 约束 + +- **严格按目标模型格式**,不混用不同模型的格式 +- **不修改原始输入**:不改写 \`\` 的任何字段;\`prompt\` 已有的分镜图提示词仅作画面参考 +- **不编造资产或台词**:只使用输入中的资产信息;无台词则标注「无台词」/ \`No dialogue\` +- **时长单位转换**:Seedance 2.0 的 \`\` 需将秒 × 1000 转为毫秒 + `, }, ]); }, diff --git a/src/router.ts b/src/router.ts index 5880361..16129e7 100644 --- a/src/router.ts +++ b/src/router.ts @@ -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); } diff --git a/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts b/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts index 79a375a..898f3b5 100644 --- a/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts +++ b/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts @@ -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; diff --git a/src/routes/assetsGenerate/polishAssetsPrompt.ts b/src/routes/assetsGenerate/polishAssetsPrompt.ts index ee96a7d..e1914bb 100644 --- a/src/routes/assetsGenerate/polishAssetsPrompt.ts +++ b/src/routes/assetsGenerate/polishAssetsPrompt.ts @@ -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; diff --git a/src/routes/production/assets/batchGenerateAssetsImage.ts b/src/routes/production/assets/batchGenerateAssetsImage.ts index dd54ac8..40b13da 100644 --- a/src/routes/production/assets/batchGenerateAssetsImage.ts +++ b/src/routes/production/assets/batchGenerateAssetsImage.ts @@ -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 = { role: { prompt: rolePrompt, diff --git a/src/routes/production/getFlowData.ts b/src/routes/production/getFlowData.ts index 0df76f0..50818bf 100644 --- a/src/routes/production/getFlowData.ts +++ b/src/routes/production/getFlowData.ts @@ -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") diff --git a/src/routes/production/storyboard/addStoryboard.ts b/src/routes/production/storyboard/addStoryboard.ts index 0aa9e7a..54aebf1 100644 --- a/src/routes/production/storyboard/addStoryboard.ts +++ b/src/routes/production/storyboard/addStoryboard.ts @@ -38,7 +38,7 @@ export default router.post( filePath: new URL(src).pathname, trackId, videoDesc, - shouldGenerateImage, + shouldGenerateImage: src ? 1 : 0, scriptId: scriptId, projectId: projectId, }); diff --git a/src/routes/production/storyboard/batchGenerateImage.ts b/src/routes/production/storyboard/batchGenerateImage.ts index a47ef8c..057b6d2 100644 --- a/src/routes/production/storyboard/batchGenerateImage.ts +++ b/src/routes/production/storyboard/batchGenerateImage.ts @@ -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)); } }, diff --git a/src/routes/production/storyboard/updateStoryboardUrl.ts b/src/routes/production/storyboard/updateStoryboardUrl.ts index 95294f9..708f43f 100644 --- a/src/routes/production/storyboard/updateStoryboardUrl.ts +++ b/src/routes/production/storyboard/updateStoryboardUrl.ts @@ -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: "更新分镜成功" })); }, diff --git a/src/routes/production/workbench/generateVideo.ts b/src/routes/production/workbench/generateVideo.ts index aae2273..c24c12b 100644 --- a/src/routes/production/workbench/generateVideo.ts +++ b/src/routes/production/workbench/generateVideo.ts @@ -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`; //视频保存路径 diff --git a/src/routes/production/workbench/generateVideoPrompt.ts b/src/routes/production/workbench/generateVideoPrompt.ts index f1990f6..79fe8d9 100644 --- a/src/routes/production/workbench/generateVideoPrompt.ts +++ b/src/routes/production/workbench/generateVideoPrompt.ts @@ -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)}, + `, }, ], }); diff --git a/src/routes/production/workbench/getGenerateData.ts b/src/routes/production/workbench/getGenerateData.ts index 2139de2..901342f 100644 --- a/src/routes/production/workbench/getGenerateData.ts +++ b/src/routes/production/workbench/getGenerateData.ts @@ -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) diff --git a/src/routes/production/workbench/selectVideo.ts b/src/routes/production/workbench/selectVideo.ts index e23e06c..fa94e52 100644 --- a/src/routes/production/workbench/selectVideo.ts +++ b/src/routes/production/workbench/selectVideo.ts @@ -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: "视频选择成功" })); }, diff --git a/src/routes/project/addDirectorManual.ts b/src/routes/project/addDirectorManual.ts new file mode 100644 index 0000000..ddb9718 --- /dev/null +++ b/src/routes/project/addDirectorManual.ts @@ -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) }); + } + }, +); diff --git a/src/routes/project/addProject.ts b/src/routes/project/addProject.ts index c1a8748..9d50ac7 100644 --- a/src/routes/project/addProject.ts +++ b/src/routes/project/addProject.ts @@ -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, diff --git a/src/routes/project/addVisual.ts b/src/routes/project/addVisual.ts deleted file mode 100644 index 958e15c..0000000 --- a/src/routes/project/addVisual.ts +++ /dev/null @@ -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) }); - } - }, -); diff --git a/src/routes/project/addVisualManual.ts b/src/routes/project/addVisualManual.ts index e8550fd..b185614 100644 --- a/src/routes/project/addVisualManual.ts +++ b/src/routes/project/addVisualManual.ts @@ -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("请勿填写重复名称的视觉手册")); } diff --git a/src/routes/project/deleteDirectorManual.ts b/src/routes/project/deleteDirectorManual.ts new file mode 100644 index 0000000..3fb77b1 --- /dev/null +++ b/src/routes/project/deleteDirectorManual.ts @@ -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 || "删除失败")); + } + }, +); diff --git a/src/routes/project/deleteVisualManual.ts b/src/routes/project/deleteVisualManual.ts index b0a1244..430c29d 100644 --- a/src/routes/project/deleteVisualManual.ts +++ b/src/routes/project/deleteVisualManual.ts @@ -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); diff --git a/src/routes/project/editDirectorlManual.ts b/src/routes/project/editDirectorlManual.ts new file mode 100644 index 0000000..ad3250f --- /dev/null +++ b/src/routes/project/editDirectorlManual.ts @@ -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) }); + } + }, +); diff --git a/src/routes/project/editProject.ts b/src/routes/project/editProject.ts index e1ec46e..f28bb4e 100644 --- a/src/routes/project/editProject.ts +++ b/src/routes/project/editProject.ts @@ -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, diff --git a/src/routes/project/editVisualManual.ts b/src/routes/project/editVisualManual.ts index 69997b0..efbaa09 100644 --- a/src/routes/project/editVisualManual.ts +++ b/src/routes/project/editVisualManual.ts @@ -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("视觉手册不存在")); } diff --git a/src/routes/project/getVisualManual.ts b/src/routes/project/getVisualManual.ts index b997b96..9d13410 100644 --- a/src/routes/project/getVisualManual.ts +++ b/src/routes/project/getVisualManual.ts @@ -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 diff --git a/src/routes/project/queryDirectorManual.ts b/src/routes/project/queryDirectorManual.ts new file mode 100644 index 0000000..a48d74b --- /dev/null +++ b/src/routes/project/queryDirectorManual.ts @@ -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) }); + } +}); diff --git a/src/types/database.d.ts b/src/types/database.d.ts index dc02a15..a8c2754 100644 --- a/src/types/database.d.ts +++ b/src/types/database.d.ts @@ -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; diff --git a/src/utils/getArtPrompt.ts b/src/utils/getArtPrompt.ts index 1b6d679..da0af90 100644 --- a/src/utils/getArtPrompt.ts +++ b/src/utils/getArtPrompt.ts @@ -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 { - const baseDir = getPath(["skills", "art_prompts", styleName]); +export function getAllArtPrompts(styleName: string, source: string): Record { + const baseDir = getPath(["skills", source, styleName]); if (!fs.existsSync(baseDir)) { return {}; diff --git a/src/utils/oss.ts b/src/utils/oss.ts index 8ae4927..370bac7 100644 --- a/src/utils/oss.ts +++ b/src/utils/oss.ts @@ -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]; diff --git a/src/utils/vm.ts b/src/utils/vm.ts index 133c4f6..c687bf2 100644 --- a/src/utils/vm.ts +++ b/src/utils/vm.ts @@ -53,7 +53,6 @@ export default function runCode(code: string, vendor?: Record) { return exports as Record; } - export function logger(logstring: string) { console.log("【VM】" + logstring); }