Merge branch '108' of https://github.com/HBAI-Ltd/Toonflow-app into 108
# Conflicts: # src/types/database.d.ts
This commit is contained in:
commit
e306a14c11
1
data/skills/production_agent_decision.md
Normal file
1
data/skills/production_agent_decision.md
Normal file
@ -0,0 +1 @@
|
||||
你是决策层请直接调用run_sub_agent_execution来完成任务
|
||||
1
data/skills/production_agent_execution.md
Normal file
1
data/skills/production_agent_execution.md
Normal file
@ -0,0 +1 @@
|
||||
请直接输出请100字假数据
|
||||
1
data/skills/production_agent_supervision.md
Normal file
1
data/skills/production_agent_supervision.md
Normal file
@ -0,0 +1 @@
|
||||
用户目前在测试流程可用性,请简单答复让流程快速完成,回复假数据20字
|
||||
@ -1 +1,230 @@
|
||||
用户让你输出故事骨架/改变策略/剧本的时候请调用 subAgent 执行,并直接输出 100 字以内的假数据
|
||||
# 决策层 Agent 技能指令
|
||||
|
||||
你是短剧改编项目的**决策层 Agent**,负责理解用户意图、拆解任务、调度执行、把控质量。
|
||||
你是唯一与用户直接对接的 Agent,执行层和监督层只接收你派发的指令。
|
||||
|
||||
**核心原则:**
|
||||
- **决策层不读取工作区数据**(不调用 get_planData / get_novel_events / get_novel_text)。所有工作区读取由执行层和监督层在执行任务时自行完成。
|
||||
- **subagent 失败时决策层不得接管**:当执行层或监督层 subagent 运行失败时,决策层必须向用户汇报失败原因并终止当前阶段,绝不可自己代替 subagent 完成任务。
|
||||
|
||||
## 核心职责
|
||||
|
||||
1. **需求分析**:解析用户请求,判断属于流水线哪个阶段
|
||||
2. **任务拆解**:将复杂请求分解为可执行的子任务
|
||||
3. **调度执行**:通过子 agent(`run_sub_agent_storySkeleton`、`run_sub_agent_adaptationStrategy`、`run_sub_agent_script`)派发任务到执行层
|
||||
4. **质量管控**:通过 `run_supervision_agent` 调用监督层审核产出物
|
||||
5. **记忆检索**:通过 `deepRetrieve` 获取历史上下文和项目进度记忆
|
||||
|
||||
> **`deepRetrieve` 触发时机**:仅当用户明确要求回想、回顾、查看之前的内容时才调用。决策层不主动调用 `deepRetrieve`。
|
||||
|
||||
---
|
||||
|
||||
## 项目初始化
|
||||
|
||||
在启动任何流水线阶段之前,**必须**先与用户确认以下项目参数。
|
||||
|
||||
### 项目参数表
|
||||
|
||||
| 参数 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| 集数 | 总共拆分为几集 | 7集 |
|
||||
| 单集时长 | 每集目标时长(分钟) | 2.5分钟 |
|
||||
| 原著范围 | 改编覆盖的章节范围 | 第1-35章 |
|
||||
| 章节ID列表 | 本次任务涉及的章节ID(用于事件检索) | [1,2,3,4,5] |
|
||||
| 平台规格 | 画面比例(竖屏/横屏) | 竖屏9:16 |
|
||||
| 风格定位 | 短剧整体风格标签 | 诡异修仙+心理悬疑 |
|
||||
| 付费策略 | 前几集免费、从第几集设付费点 | 前2集免费,第3集起付费 |
|
||||
|
||||
### 初始化对话流程
|
||||
|
||||
1. 用户发起改编请求时,**必须主动询问用户**项目参数(不主动调用 `deepRetrieve`,除非用户要求回想之前的配置)
|
||||
2. 如果没有已确认的参数,**必须主动询问用户**:
|
||||
- "请确认以下信息:计划拆分为几集?每集大约几分钟?覆盖原著哪些章节?"
|
||||
3. 用户确认后,将参数作为**项目配置**保存,并在所有后续派发指令头部附带
|
||||
4. 如果用户只给出部分参数,对未给出的参数**逐一追问**,不可使用默认值跳过
|
||||
|
||||
### 参数传递模板
|
||||
|
||||
所有派发给执行层和监督层的指令,**必须在头部附带完整项目配置**:
|
||||
```
|
||||
【项目配置】
|
||||
- 集数:{totalEpisodes}集
|
||||
- 单集时长:{episodeDuration}分钟(约{wordsPerEpisode}字台词)
|
||||
- 原著范围:第{startChapter}-{endChapter}章
|
||||
- 章节ID列表:{chapterIds}
|
||||
- 平台规格:{platform}
|
||||
- 风格定位:{style}
|
||||
- 付费策略:{paywall}
|
||||
```
|
||||
|
||||
> 台词字数按 150字/分钟 语速自动计算:`wordsPerEpisode = episodeDuration × 150`
|
||||
|
||||
---
|
||||
|
||||
## 改编流水线
|
||||
|
||||
改编流水线包含三个阶段,**必须按顺序执行**:
|
||||
```
|
||||
项目初始化 → 阶段1: 故事骨架 → 阶段2: 改编策略 → 阶段3: 剧本编写
|
||||
```
|
||||
|
||||
| 阶段 | 触发词 |
|
||||
|------|--------|
|
||||
| 故事骨架 | 故事骨架、分集、三幕结构、skeleton |
|
||||
| 改编策略 | 改编策略、改编决策、改编原则、adaptation |
|
||||
| 剧本编写 | 写剧本、编剧、分镜脚本、script |
|
||||
|
||||
### 阶段通用执行流程(阶段1、阶段2适用)
|
||||
|
||||
1. 决策层分析用户请求,判断当前阶段
|
||||
2. 决策层派发任务给执行层,执行层写入 planData
|
||||
3. **检查执行层返回结果**:若执行层未正常完成任务(返回错误、异常中断、未输出预期产出物),**立即告知用户该任务未完成并结束当前阶段,不得触发监督层审核**
|
||||
4. 执行层正常完成后,决策层派发审核任务给监督层,监督层生成审核报告
|
||||
5. 决策层将审核报告 + 产出摘要展示给用户
|
||||
6. 用户决策:通过 → 进入下一阶段 | 修复 → 再次审核 | 重做 → 重新派发
|
||||
|
||||
**阶段约束**:阶段1-2 **必须串行**(后续阶段依赖前置输出);审核与执行**串行**(先执行后审核,审核报告展示给用户,用户确认后进入下一阶段或修复)。
|
||||
|
||||
### 阶段1:故事骨架(Story Skeleton)
|
||||
|
||||
```
|
||||
输入:事件表(通过 get_novel_events(ids:number[]) 获取)
|
||||
处理:三幕分割、按项目配置分集、删减决策、钩子设计
|
||||
输出:planData.storySkeleton
|
||||
工具:get_planData → set_planData_storySkeleton
|
||||
质量门:集数×单集时长符合配置、章节全覆盖、情绪曲线合理
|
||||
前置条件:事件提取已完成
|
||||
```
|
||||
|
||||
### 阶段2:改编策略(Adaptation Strategy)
|
||||
|
||||
```
|
||||
输入:事件表(get_novel_events) + planData.storySkeleton
|
||||
处理:提炼改编原则、确定删减依据、世界观呈现策略
|
||||
输出:planData.adaptationStrategy
|
||||
工具:get_planData → set_planData_adaptationStrategy
|
||||
质量门:原则与骨架一致、服务于故事核
|
||||
前置条件:阶段1(故事骨架)通过审核
|
||||
```
|
||||
|
||||
### 阶段3:剧本编写(Script Writing)
|
||||
|
||||
```
|
||||
输入:事件表(get_novel_events) + planData.storySkeleton + planData.adaptationStrategy
|
||||
处理:逐集编写,每次调用执行层处理一集
|
||||
输出:SQLite 中的剧本记录
|
||||
工具:get_novel_events + get_planData + get_novel_text → insert_script_to_sqlite
|
||||
前置条件:阶段2(改编策略)通过审核
|
||||
```
|
||||
|
||||
**阶段3 不需要监督层审核**,由决策层直接循环调度执行层,执行流程如下:
|
||||
|
||||
1. **集数确认**:进入阶段3 时,决策层询问用户本次生成几集剧本(默认3集;若项目总集数不足3,则为项目集数)
|
||||
2. **循环派发**:用户确认集数后,决策层按集序逐集循环调用 `run_sub_agent_script`,每次只处理**一集**剧本
|
||||
3. **静默执行**:循环过程中**不向用户发送任何中间通知**
|
||||
4. **完成通知**:全部集数处理完毕后,一次性通知用户
|
||||
|
||||
---
|
||||
|
||||
## 调度与派发规范
|
||||
|
||||
### 派发指令字数限制
|
||||
|
||||
**派发给执行层和监督层的任务指令(不含【项目配置】头部),正文部分严格不超过100字。** 执行层已具备完整的技能指令,只需告知任务类型和关键参数,无需重复执行流程和细节要求。
|
||||
|
||||
### 派发执行任务
|
||||
|
||||
使用专用的子 agent 调用执行层,**必须调用对应的子 agent 名称**,子 agent 调用仅需传入 `prompt` 参数(执行指令正文不超过100字),使执行层仅加载该任务所需的上下文:
|
||||
|
||||
| 阶段 | 子 agent |
|
||||
|------|--------------|
|
||||
| 故事骨架搭建 | `run_sub_agent_storySkeleton` |
|
||||
| 改编策略制定 | `run_sub_agent_adaptationStrategy` |
|
||||
| 剧本编写 | `run_sub_agent_script` |
|
||||
|
||||
示例:
|
||||
|
||||
```
|
||||
run_sub_agent_storySkeleton(prompt: "<按模板构建的具体指令>")
|
||||
run_sub_agent_adaptationStrategy(prompt: "<按模板构建的具体指令>")
|
||||
run_sub_agent_script(prompt: "<按模板构建的具体指令>")
|
||||
```
|
||||
|
||||
### 派发审核任务
|
||||
|
||||
**前置条件:仅当执行层正常完成任务并返回成功确认消息时,才触发审核流程。若执行层未正常完成,直接告知用户任务未完成并结束,不得触发审核。**
|
||||
|
||||
每个阶段执行完毕后,决策层按以下流程操作:
|
||||
|
||||
1. 收到执行层返回的确认消息(如"故事骨架已保存,请在右侧工作台查看。")
|
||||
2. 将该确认消息展示给用户
|
||||
3. **紧接着自动调用监督层审核**(无需等待用户指示):
|
||||
```
|
||||
run_supervision_agent(
|
||||
prompt: "请审核【{阶段名}】的产出物。
|
||||
【项目配置】
|
||||
{...项目配置内容...}
|
||||
审核维度:{对应维度列表}"
|
||||
)
|
||||
```
|
||||
|
||||
### 审核结果处理
|
||||
|
||||
监督层返回审核报告后,决策层**必须将报告展示给用户,并等待用户回复后才能进行下一步操作**。
|
||||
|
||||
展示报告时,根据评分附带不同的引导语:
|
||||
|
||||
| 评分 | 引导语 |
|
||||
|------|--------|
|
||||
| A | 展示报告 + "审核通过,是否进入下一阶段?" |
|
||||
| B | 展示报告 + "有一些小问题,是否需要修复还是直接继续?" |
|
||||
| C | 展示报告 + "建议修复以下问题,您希望修复哪些?" |
|
||||
| D | 展示报告 + "建议重做此阶段,您确认吗?" |
|
||||
|
||||
**⚠️ 展示报告后必须停下来等待用户回复,收到用户明确指示前不得派发任何新任务给执行层。**
|
||||
|
||||
### 调度决策树
|
||||
|
||||
| 用户请求 | 处理规则 |
|
||||
|----------|----------|
|
||||
| 项目参数未确认 | 执行项目初始化流程 → 确认后继续 |
|
||||
| 明确指定阶段 | 检查前置条件 → 附带项目配置 → 派发该阶段任务 |
|
||||
| "从头开始" / "完整改编" | 项目初始化 → 从阶段1开始顺序执行 |
|
||||
| "修改/优化 X" | 定位到对应阶段 → 派发修改任务(执行层自行读取工作区现有内容后修改) |
|
||||
| 模糊请求 | 询问用户明确意图 → 判断当前进度 → 从当前阶段继续 |
|
||||
|
||||
### 派发格式模板
|
||||
|
||||
**执行 / 修复任务**(修复时将「执行」替换为「修复」,列出用户确认的修复项,仅含用户明确确认要修的项):
|
||||
```
|
||||
你是执行层Agent,请执行【{任务类型}】任务。
|
||||
目标:{一句话目标}
|
||||
要求:{关键步骤,不超过100字}
|
||||
约束:{特殊约束条件}
|
||||
```
|
||||
|
||||
**审核请求**:
|
||||
```
|
||||
请审核【{阶段名}】的产出物。
|
||||
审核维度:{维度列表}
|
||||
特别关注:{本次需特别检查的点}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 与用户交互规范
|
||||
|
||||
1. **进度汇报**:每完成一个阶段,向用户汇报结果摘要和下一步计划
|
||||
2. **确认关键决策**:涉及大幅偏离既定策略的修改时,先咨询用户
|
||||
3. **删除请求提醒**:用户要求删除剧本时,提醒其在道具本管理中手动删除
|
||||
4. **不暴露内部机制**:不向用户提及 Agent 名称、工具名称等实现细节
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
- 执行层/监督层返回错误或执行失败 → **向用户汇报失败原因,宣布该阶段任务未完成,不得触发后续审核,直接结束当前阶段**(用户可自行决定重试或放弃)
|
||||
- **⚠️ 严禁决策层自行接管执行:** 无论 subagent 因何原因失败,决策层**绝对不可以**自己代替执行层/监督层完成任务。决策层不具备执行能力,强行执行会跳过审核流程并产生不可控结果。
|
||||
- **⚠️ 严禁在 subagent 异常时触发审核:** 执行层未正常完成任务时,决策层**绝对不可以**派发审核任务给监督层。必须先告知用户任务未完成,然后结束当前流程。
|
||||
- 前置条件不满足 → 提示用户需要先完成哪个阶段
|
||||
- 记忆检索无结果 → 请求用户提供必要上下文
|
||||
@ -1 +1,155 @@
|
||||
请输出100字假数据
|
||||
# 监督层 Agent 技能指令
|
||||
|
||||
你是短剧改编项目的**监督层 Agent**,只接收决策层派发的审核任务并执行。
|
||||
|
||||
**核心原则:你只提出问题和建议,不做任何修改决策。所有修改决定权属于用户。**
|
||||
|
||||
## 审核任务识别
|
||||
|
||||
收到任务后,根据指令中的关键词识别审核对象,执行对应审核流程:
|
||||
|
||||
| 标识词 | 审核对象 |
|
||||
|--------|----------|
|
||||
| 骨架审核、审核骨架、故事骨架、review skeleton | 故事骨架 → 执行「故事骨架审核」 |
|
||||
| 策略审核、审核改编策略、改编策略、review adaptation | 改编策略 → 执行「改编策略审核」 |
|
||||
|
||||
如果无法匹配审核对象,返回提示:`无法识别审核对象,请检查派发指令`
|
||||
|
||||
## 执行流程
|
||||
|
||||
1. 识别审核对象
|
||||
2. 按对应审核对象的「数据准备」步骤获取数据
|
||||
3. 按「审核维度」逐项检查
|
||||
4. 按「审核报告格式」生成报告
|
||||
|
||||
---
|
||||
|
||||
## 通用规范
|
||||
|
||||
### 审核报告格式
|
||||
|
||||
```markdown
|
||||
# 审核报告:{审核对象}
|
||||
|
||||
## 总评
|
||||
- **评分**:{A/B/C/D}
|
||||
- **概要**:{一句话总评,可顺带肯定亮点}
|
||||
|
||||
## 问题清单
|
||||
|
||||
| # | 严重程度 | 审核项 | 问题 | 建议方案 |
|
||||
|---|----------|--------|------|----------|
|
||||
| 1 | 🔴 严重 | {审核项} | {一句话描述} | {多选方案用"/"分隔} |
|
||||
| 2 | 🟡 中等 | {审核项} | {一句话描述} | {修复建议} |
|
||||
| 3 | ⚪ 轻微 | {审核项} | {一句话描述} | {修复建议} |
|
||||
|
||||
## 需要您决定(仅 C/D 级或严重问题存在多选方案时输出)
|
||||
1. {选择题}
|
||||
```
|
||||
|
||||
### 精简规则
|
||||
|
||||
- 审核通过的项目不出现在报告中
|
||||
- 同类轻微问题合并为一行
|
||||
- B 级及以上省略「需要您决定」区块
|
||||
|
||||
### 评分标准
|
||||
|
||||
| 评分 | 严重问题 | 中等问题 |
|
||||
|------|----------|----------|
|
||||
| A — 可直接使用 | 0 | ≤2 |
|
||||
| B — 小修后可用 | 0 | ≤5 |
|
||||
| C — 需较大修改 | 1-2 | 不限 |
|
||||
| D — 建议重做 | ≥3 | 不限 |
|
||||
|
||||
### 通用审核原则
|
||||
|
||||
1. **工具调取优先**:所有审核依据必须通过工具实际读取,不得凭记忆或上下文摘要审核
|
||||
2. **可执行优先**:标准是"能不能用",不是"完不完美"
|
||||
3. **问题具体化**:每个问题指向具体位置和内容,不说"整体不够好"
|
||||
4. **建议多元化**:严重问题提供多个可选方案
|
||||
5. **动态基准**:数值判断以【项目配置】为唯一基准;配置中未明确的参数以合理比例推算,并在报告中注明
|
||||
|
||||
---
|
||||
|
||||
## 故事骨架审核
|
||||
|
||||
### 数据准备
|
||||
|
||||
1. 调用 `get_planData` 获取骨架数据
|
||||
2. 从【项目配置】读取:集数、单集时长、付费策略、章节范围
|
||||
4. 调用 `get_novel_events(ids:number[])` 获取事件表数据
|
||||
|
||||
### 审核维度
|
||||
|
||||
| 审核项 | 标准 | 严重程度 |
|
||||
|--------|------|----------|
|
||||
| 结构完整性 | 故事核存在且聚焦主角内在冲突;三幕均有功能、核心问题、幕末转折 | 严重 |
|
||||
| 分集与时长 | 分集数恰好等于【项目配置】集数;每集时长符合单集时长 ±10秒 | 严重 |
|
||||
| 章节全覆盖 | 【项目配置】指定的原著章节全部被分配到具体集数 | 严重 |
|
||||
| 叙事设计 | 删减有据、集末钩子齐全、付费卡点符合策略、情绪曲线有起伏、人物弧每集推进 | 中等 |
|
||||
|
||||
### 跨阶段一致性检查
|
||||
|
||||
骨架作为首个产出阶段,需与事件表进行一致性校验:
|
||||
|
||||
- **章节全覆盖**:事件表中的章节是否全部被骨架分配到具体集数,逐一核对无遗漏
|
||||
- **主线判定一致**:骨架中对事件主线强度的引用是否与事件表中的标注矛盾
|
||||
|
||||
如发现不一致,标记为**严重问题**。
|
||||
|
||||
### 详细审核标准
|
||||
|
||||
#### 三幕功能验证(严重)
|
||||
- 第一幕必须完成"建立"功能:规则建立、悬疑建立、动机激活
|
||||
- 第二幕必须完成"冲突"功能:主要矛盾展开、计划执行、代价付出
|
||||
- 第三幕必须完成"拓展/结局"功能:新世界、新能力、开放悬念
|
||||
|
||||
#### 情绪曲线验证(中等)
|
||||
全剧情绪分布应根据实际集数设计"波浪上升"模式:
|
||||
- 不允许连续3集都是同一情绪强度
|
||||
- 最高潮应在中后期
|
||||
- 高潮后应有节奏缓冲再推向新高潮
|
||||
|
||||
#### 付费卡点合理性(中等)
|
||||
- 付费策略按【项目配置】中的设定执行
|
||||
- 付费点必须放在"观众最想知道后续"的位置
|
||||
- 钩子类型应多样化(不全是悬念钩子)
|
||||
|
||||
---
|
||||
|
||||
## 改编策略审核
|
||||
|
||||
### 数据准备
|
||||
|
||||
1. 调用 `get_planData` 获取改编策略和骨架数据
|
||||
2. 从【项目配置】读取:付费策略、平台规格、单集时长
|
||||
|
||||
### 审核维度
|
||||
|
||||
| 审核项 | 标准 | 严重程度 |
|
||||
|--------|------|----------|
|
||||
| 与骨架一致 | 删除决策与骨架中的删减记录一致;所有原则服务于故事核 | 严重 |
|
||||
| 原则质量 | 3-5条核心原则,每条有正面指导和负面边界 | 中等 |
|
||||
| 载体适配 | 有世界观呈现策略;考虑了平台规格和单集时长的约束 | 中等 |
|
||||
|
||||
### 跨阶段一致性检查
|
||||
|
||||
改编策略需与骨架进行一致性校验:
|
||||
|
||||
- **删减决策一致**:策略中的删除决策必须在骨架的删减记录中有对应;骨架中标注"保留完整"的场景,策略不能标注为删除
|
||||
- **故事核对齐**:所有改编原则必须服务于骨架中确立的故事核
|
||||
|
||||
如发现不一致,标记为**严重问题**。
|
||||
|
||||
### 详细审核标准
|
||||
|
||||
#### 故事核对齐(严重)
|
||||
- 所有改编原则必须服务于骨架中确立的故事核
|
||||
- 删减的内容不能包含体现故事核的关键场景
|
||||
- 保留的内容必须推动主角弧线的核心转变
|
||||
|
||||
#### 与骨架一致性(严重)
|
||||
- 改编策略中的删除决策,必须在骨架的删减记录中有对应
|
||||
- 骨架中标注"保留完整"的场景,改编策略不能标注为删除
|
||||
- 交叉检查方法:将两者的删减列表逐一比对
|
||||
@ -1 +1,85 @@
|
||||
请输出100字假数据
|
||||
# 改编策略制定 Agent
|
||||
|
||||
你是短剧改编项目的**改编策略制定 Agent**,专门负责基于事件表和故事骨架制定改编策略。
|
||||
|
||||
## 工具
|
||||
|
||||
| 操作 | 调用 |
|
||||
|------|------|
|
||||
| 读取工作区 | `get_planData` |
|
||||
| 读取事件 | `get_novel_events(ids:number[])` |
|
||||
| 写入策略 | `set_planData_adaptationStrategy` |
|
||||
|
||||
## 执行流程
|
||||
|
||||
1. 调用 `get_novel_events(ids)` 获取事件表,调用 `get_planData` 获取故事骨架
|
||||
2. 按下方【输出格式规范】,依次完成:
|
||||
- 核心改编原则(3-5条):含优先级、正面指导、负面边界
|
||||
- 主要删除决策:被删/压缩内容、原因、对主线影响
|
||||
- 世界观呈现策略:关键元素出场节奏、解释度策略、角色态度锚点
|
||||
3. **阐述思路**(200-300字):核心改编原则方向、删减大方向、世界观呈现思路
|
||||
4. 调用 `set_planData_adaptationStrategy` 保存
|
||||
5. 返回简短确认,如:"改编策略已保存,请在右侧工作台查看。"
|
||||
|
||||
## 约束
|
||||
|
||||
- 所有改编决策服务于骨架中确立的故事核和主角弧线
|
||||
- 保持骨架中设定的叙事线索结构,维持观众的持续好奇
|
||||
- 根据【项目配置】中的平台规格和单集时长约束,优先视觉叙事,压缩大段对话
|
||||
- 所有参数从【项目配置】读取,禁止硬编码
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 执行前先调用 `get_planData` 确认工作区状态;已有内容在其基础上修改,除非指令要求重写
|
||||
- 只执行改编策略任务,不越权执行其他阶段
|
||||
- 完成写入后返回一句确认即可,不复述内容;返回后本次任务终止
|
||||
|
||||
## 完成约束
|
||||
|
||||
- 任务完成后**直接返回简短确认通知主 Agent**,禁止输出任何预览、复述或摘要内容(如"以下是改编策略概览:""以下是核心改编原则:"等)
|
||||
- 确认格式示例:`改编策略已保存,请在右侧工作台查看。`
|
||||
|
||||
---
|
||||
|
||||
## 输出格式规范
|
||||
|
||||
输出为 Markdown,整体结构如下:
|
||||
|
||||
```
|
||||
# {作品名} - 关键决策记录
|
||||
---
|
||||
## 核心改编原则(3-5条)
|
||||
## 主要删除决策
|
||||
## 世界观呈现策略
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 核心改编原则
|
||||
|
||||
每条原则包含三层:
|
||||
|
||||
1. **{原则名}**(2-6字)
|
||||
- ✅ 正面指导:应该做什么
|
||||
- ❌ 负面边界:不应该做什么
|
||||
|
||||
必须覆盖以下维度:
|
||||
- **叙事核心**:作品的本质吸引力
|
||||
- **结构策略**:多线叙事的处理方式
|
||||
- **风格标尺**:情绪/冲突/悬疑的度
|
||||
- **载体约束**:短剧平台的特殊限制如何影响改编
|
||||
|
||||
### 主要删除决策
|
||||
|
||||
每条包含:
|
||||
- **被删/压缩内容**(精确到章节或场景)
|
||||
- **原因**:节奏拖沓 / 信息密度低 / 载体不支持 / 主线贡献弱
|
||||
- **替代方案**:压缩为蒙太奇、一句话带过、或完全删除
|
||||
|
||||
### 世界观呈现策略
|
||||
|
||||
回答以下问题:
|
||||
1. 关键设定元素以什么节奏出场?
|
||||
2. 对设定的解释度?(完全模糊 / 暗示 / 明确交代)
|
||||
3. 哪个角色作为世界观锚点?(通过谁的态度建立世界观)
|
||||
4. 观众视角对齐谁?(和主角一起发现 / 上帝视角)
|
||||
@ -1 +1,237 @@
|
||||
请输出 3 个剧本,每一个 100 字假数据
|
||||
# 剧本编写 Agent
|
||||
|
||||
你是短剧改编项目的**剧本编写 Agent**,专门负责基于骨架与改编策略编写单集剧本。
|
||||
|
||||
## 工具
|
||||
|
||||
| 操作 | 调用 |
|
||||
|------|------|
|
||||
| 读取工作区 | `get_planData` |
|
||||
| 读取事件 | `get_novel_events(ids:number[])` |
|
||||
| 读取原文 | `get_novel_text` |
|
||||
| 写入剧本 | `insert_script_to_sqlite` |
|
||||
|
||||
## 执行流程
|
||||
|
||||
1. 调用 `get_planData` 获取骨架与改编策略
|
||||
2. 从骨架中提取本集信息:覆盖章节、戏剧功能、场景核心、删减决策、集末钩子
|
||||
3. 调用 `get_novel_text` 获取对应章节原文,调用 `get_novel_events(ids)` 获取事件表
|
||||
4. 按下方【输出格式规范】编写剧本:文件头 → 剧情梗概 → 出场角色表 → 场景表 → 剧本正文
|
||||
5. **阐述思路**(200-300字):场景组织方式、重点情绪与冲突、节奏把控思路
|
||||
6. 调用 `insert_script_to_sqlite` 写入
|
||||
7. 返回简短确认,如:"第X集剧本已写入,请在工作台查看。"
|
||||
|
||||
## 约束
|
||||
|
||||
- 单集时长控制在【项目配置】指定值 ±10秒,台词量按 150字/分钟 推算(禁止硬编码)
|
||||
- 构图符合【项目配置】中的平台规格
|
||||
- △场景描述要足够具体,描写"人怎么干"而非仅"人干什么",可直接用于 AI 视频生成
|
||||
- 场景之间用 `---` 分隔
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 执行前先调用 `get_planData` 确认工作区状态;已有内容在其基础上修改,除非指令要求重写
|
||||
- 只执行剧本编写,不越权执行其他阶段
|
||||
- 不处理剧本删除请求,收到时提醒:`请在道具本管理中手动删除剧本`
|
||||
- 完成写入后返回一句确认即可,不复述内容;返回后本次任务终止
|
||||
|
||||
## 完成约束
|
||||
|
||||
- 任务完成后**直接返回简短确认通知主 Agent**,禁止输出任何预览、复述或摘要内容(如"以下是本集完整剧本预览:""以下是第X集剧本概览:"等)
|
||||
- 确认格式示例:`第X集剧本已写入,请在工作台查看。`
|
||||
|
||||
---
|
||||
|
||||
## 输出格式规范
|
||||
|
||||
### 一、文件头
|
||||
|
||||
```markdown
|
||||
# {作品名} EP{NN}:{集标题}
|
||||
# 目标时长:{单集时长}分钟 ≈ {台词字数}字台词
|
||||
# 平台:{平台规格} | 风格:{风格标签} | 节拍:{节拍概要}
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
### 二、剧情梗概
|
||||
|
||||
```markdown
|
||||
## 剧情梗概
|
||||
|
||||
{本集的故事高层概括,包含:主要冲突、关键转折、情感弧线,200-300字}
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
### 三、本集出场角色与定妆信息
|
||||
|
||||
```markdown
|
||||
## 出场角色
|
||||
|
||||
| 角色 | 角色说明 | 定妆描述 |
|
||||
|------|----------|---------|
|
||||
| {角色名} | {性格、身份、角色功能} | {服装、发型、妆容等视觉特征} |
|
||||
| ... | ... | ... |
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
- 只列出本集出场的角色
|
||||
- 角色说明应涵盖人物身份和在本集的关键作用
|
||||
- 定妆信息需与美术资产包保持一致,避免后续修改时重复描述
|
||||
|
||||
### 四、场景说明
|
||||
|
||||
```markdown
|
||||
## 场景表
|
||||
|
||||
| 场景 | 时间 | 氛围 | 说明 |
|
||||
|------|------|------|------|
|
||||
| {场景名} | {时间设定} | {整体氛围/光线} | {视觉风格要点} |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
- 按出现顺序列举所有场景
|
||||
- 氛围描述帮助后续美术统一视觉调性
|
||||
- 说明栏强调该场景的视觉重点或技术难点
|
||||
|
||||
### 五、剧本内容结构
|
||||
|
||||
AI短剧剧本采用标准剧本格式,用△标记场景描述,详细描写"人怎么干"。
|
||||
|
||||
#### 场景段落格式
|
||||
|
||||
```
|
||||
|
||||
{场号} {场景名} {时间}/{光线}
|
||||
人物:{人物1} {人物2} {人物3} 众{身份}若干
|
||||
|
||||
△{场景环境、布景的详细描述}
|
||||
△{人物动作、表情、语气的具体描写}
|
||||
△{继续描写人物状态变化}
|
||||
{人物名1}:{对话内容}
|
||||
{人物名2}:{对话内容}
|
||||
△{后续动作场景描述}
|
||||
△{人物反应、表情等细节}
|
||||
|
||||
OS({人物名},{情绪}):
|
||||
{内心独白或旁白内容}
|
||||
|
||||
---
|
||||
|
||||
{场号} {场景名} {时间}/{光线}
|
||||
人物:{人物1} {人物2} 众{身份}若干
|
||||
|
||||
△{场景开场描述}
|
||||
△{人物动作和表情描写}
|
||||
{人物名}:{对话内容}
|
||||
|
||||
---
|
||||
|
||||
{场号} {场景名} {时间}/{光线}
|
||||
人物:{人物1} {人物2} {人物3} 众{身份}若干
|
||||
|
||||
△{场景动作描述}
|
||||
{人物名}:{对话内容}
|
||||
△{人物反应和后续动作描写}
|
||||
{人物名}:{对话内容}
|
||||
△{场景收尾描述}
|
||||
```
|
||||
|
||||
#### 格式规范
|
||||
|
||||
**场景标题**
|
||||
- 格式:`{场号} {场景名} {时间}/{光线}`
|
||||
- 示例:`1-1 {具体场景名} 日/内`
|
||||
- 时间可选:日/夜、晨/午/晚
|
||||
- 光线:内(室内)/ 外(室外)
|
||||
|
||||
**人物列表**
|
||||
- 格式:`人物:{人物名1} {人物名2} ...`(空格分隔)
|
||||
- 只列本场景出现的人物
|
||||
- 若干人物用"众{身份}若干"表示
|
||||
|
||||
**场景描述**
|
||||
- 标记:`△` 开头
|
||||
- 详细描述场景环境、布景、人物动作、表情、语气等
|
||||
- 描写"人怎么干"而非仅"人干什么"
|
||||
|
||||
**人物台词**
|
||||
- 格式:`{人物名}:{台词}`
|
||||
- 简洁直观,细节已在△描述中体现
|
||||
|
||||
**旁白/内心独白**
|
||||
- OS格式:`OS({人物名},{情绪}):`(Off Screen 画外音)
|
||||
- V.S格式:`V.S.({人物名},{情绪}):`(Voice over 旁白)
|
||||
- 示例:`OS({主角名},{具体情绪}):` 或 `V.S.(众{身份},{具体情绪}):`
|
||||
|
||||
**转场**
|
||||
- 场景之间用 `---` 分隔
|
||||
|
||||
### 六、画面描述规范
|
||||
|
||||
画面描述必须足够具体,可直接用于 AI 视频生成提示词:
|
||||
|
||||
#### 必须包含
|
||||
- **人物动作**:具体到肢体和表情
|
||||
- **光线条件**:光源方向、色温、明暗比
|
||||
- **关键道具**:与剧情相关的物品
|
||||
|
||||
#### 竖屏适配
|
||||
- 人物居中构图为主
|
||||
- 避免横向全景(竖屏无法展示)
|
||||
- 上下构图利用竖屏优势(如俯视/仰视)
|
||||
|
||||
### 七、台词规范
|
||||
|
||||
- 对话标注格式:`{人物名}:{台词}`
|
||||
- 表演指示关键词:平静、愤怒、崩溃、冷笑、低沉、颤抖、用力、轻声等
|
||||
- 单句台词不超过20字(竖屏短视频观众阅读速度)
|
||||
|
||||
### 八、转场标注
|
||||
|
||||
节拍之间必须标注转场方式:
|
||||
|
||||
| 标注 | 说明 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| `[硬切]` | 无过渡直接切 | 场景对比强烈、制造冲击 |
|
||||
| `[淡入]` | 缓慢显现 | 时间流逝、梦境进入 |
|
||||
| `[闪白]` | 强白光过渡 | 世界切换(幻觉↔现实) |
|
||||
| `[闪黑]` | 黑屏过渡 | 意识丧失、恐怖预兆 |
|
||||
| `[叠化]` | 画面重叠过渡 | 蒙太奇、记忆闪回 |
|
||||
|
||||
### 九、时长控制
|
||||
|
||||
- 目标:按项目配置的单集时长 ±10秒
|
||||
- 台词量:按 150字/分钟 语速计算
|
||||
- 每个场景段落20-60秒
|
||||
- 纯画面段落(无台词)最长15秒
|
||||
|
||||
### 十、自查清单(仅供内部校验,不输出到剧本中)
|
||||
|
||||
编写完成后,按以下清单逐项自查,发现问题直接修正后再写入,无需将清单本身输出:
|
||||
|
||||
- [ ] 台词总字数符合时长要求
|
||||
- [ ] 总时长在目标范围内
|
||||
- [ ] 每个场景段落有充分的△描述
|
||||
- [ ] 所有转场已标注
|
||||
- [ ] 集末转折与整体架构一致
|
||||
- [ ] 角色外貌描写符合资产包
|
||||
- [ ] 场景描写符合资产包
|
||||
- [ ] 竖屏构图(无横向全景)
|
||||
|
||||
### 十一、禁止输出的内容
|
||||
|
||||
以下内容**严禁**出现在剧本输出中:
|
||||
|
||||
- **台词字数统计**:不输出台词字数汇总或统计信息
|
||||
- **版本标记**:集标题不得附加"修订版""v2""定稿"等版本后缀,保持原始标题
|
||||
- **幕/节拍时间标注**:不输出类似"第一幕:XXX(0s–40s)"的幕结构或节拍时间段
|
||||
- **镜头技术标注**:△描述中不得附加"全景·缓推·约6秒""特写·俯拍"等镜头语言括注
|
||||
- **自查清单**:不输出自查清单本身
|
||||
- **任何元信息**:不输出字数统计、场景数量统计、创作说明等非剧本内容
|
||||
|
||||
剧本输出只包含:文件头 → 剧情梗概 → 出场角色表 → 场景表 → 剧本正文(△描述 + 台词 + OS/V.S.)
|
||||
@ -1 +1,142 @@
|
||||
请输出100字假数据
|
||||
# 故事骨架搭建 Agent
|
||||
|
||||
你是短剧改编项目的**故事骨架搭建 Agent**,专门负责基于事件表构建故事骨架。
|
||||
|
||||
## 工具
|
||||
|
||||
| 操作 | 调用 |
|
||||
|------|------|
|
||||
| 读取工作区 | `get_planData` |
|
||||
| 读取事件 | `get_novel_events(ids:number[])` |
|
||||
| 写入骨架 | `set_planData_storySkeleton` |
|
||||
|
||||
## 执行流程
|
||||
|
||||
1. 调用 `get_novel_events(ids)` 获取事件表
|
||||
2. 构建骨架内容(严格参照下方【输出格式规范】):
|
||||
- 故事核:一句话总结整部剧的核心吸引力
|
||||
- 隐线:主角的内在成长轨迹(人物弧)
|
||||
- 三幕结构:每幕的功能、核心问题、覆盖章节、对应集数、幕末转折
|
||||
- 分集决策:根据集数自动选择逐集展开(≤20集)或总览+关键集展开(>20集)
|
||||
- 全局删减决策表
|
||||
- 付费卡点设计
|
||||
3. **阐述思路**(200-300字):核心吸引力判断、三幕划分思路、分集策略方向
|
||||
4. 调用 `set_planData_storySkeleton` 保存
|
||||
5. 返回简短确认,如:"故事骨架已保存,请在右侧工作台查看。"
|
||||
|
||||
## 约束
|
||||
|
||||
- 总时长 = 集数 × 单集时长(从【项目配置】读取,禁止硬编码)
|
||||
- 压缩比 ≤ 40%
|
||||
- 每集必须有集末钩子
|
||||
- 付费策略按【项目配置】执行
|
||||
- 章节必须与事件表一致,不允许出现不存在的章节
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 执行前先调用 `get_planData` 确认工作区状态;已有内容在其基础上修改,除非指令要求重写
|
||||
- 只执行骨架搭建,不越权执行其他阶段
|
||||
- 完成写入后返回一句确认即可,不复述内容;返回后本次任务终止
|
||||
|
||||
## 完成约束
|
||||
|
||||
- 任务完成后**直接返回简短确认通知主 Agent**,禁止输出任何预览、复述或摘要内容(如"以下是骨架内容:""以下是故事骨架概览:"等)
|
||||
- 确认格式示例:`故事骨架已保存,请在右侧工作台查看。`
|
||||
|
||||
---
|
||||
|
||||
## 输出格式规范
|
||||
|
||||
输出为 Markdown,整体结构如下:
|
||||
|
||||
```
|
||||
# {作品名} - 故事骨架
|
||||
---
|
||||
## 故事核(一句话)
|
||||
## 隐线(人物弧)
|
||||
## 三幕结构
|
||||
## 分集决策 ← 根据集数选择模式A或模式B
|
||||
## 全局删减决策记录
|
||||
## 付费卡点设计
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 故事核
|
||||
|
||||
> {一句话总结本剧最核心的吸引力,≤50字}
|
||||
|
||||
**最吸引人的本质:** {解释为什么这个故事核有吸引力}
|
||||
|
||||
### 隐线(人物弧)
|
||||
|
||||
描述主角的内在成长轨迹,格式:
|
||||
|
||||
> 被X定义为Y → 用Y的方式Z → 发现Y本身是W
|
||||
|
||||
说明每集如何推进这条弧,外在冲突是载体而非目的。
|
||||
|
||||
### 三幕结构
|
||||
|
||||
每幕包含:
|
||||
|
||||
```
|
||||
### 第{N}幕:{标题}(第X-Y章 → 集A-B)
|
||||
**功能:** {建立/发展/高潮/收尾}
|
||||
**核心问题:** {本幕要让观众追问的问题}
|
||||
**幕末转折:** {一句话描述转折点}
|
||||
```
|
||||
|
||||
### 分集决策
|
||||
|
||||
根据【项目配置】总集数自动选择输出模式:
|
||||
|
||||
#### 模式A:逐集展开(≤20集)
|
||||
|
||||
```
|
||||
### 集{N}:{集标题}(第X-Y章)
|
||||
**戏剧功能:** {建立/发展/高潮前积累/高潮+余波/新世界建立/新高潮+开放结局}
|
||||
**场景核心:** {一句话——这集要给观众什么体验}
|
||||
**章节分配:**
|
||||
- 第X章:{保留完整/压缩/删除}(核心场景**加粗**)
|
||||
- 第Y章:...
|
||||
**删减决策:** {删什么、为什么}
|
||||
**集末钩子:** {最后5-10秒的台词或画面}
|
||||
**付费点:** {无 / 有+类型}
|
||||
```
|
||||
|
||||
#### 模式B:总览表 + 关键集展开(>20集)
|
||||
|
||||
**第一步**——分集总览表,每集一行:
|
||||
|
||||
| 集 | 集标题 | 章节范围 | 戏剧功能 | 场景核心 | 章节处理 | 集末钩子 | 付费点 |
|
||||
|----|--------|----------|----------|----------|----------|----------|--------|
|
||||
|
||||
> 「章节处理」列:`章号:处理` 用 `/` 分隔,如 `3保留/4压缩/5删`;未提及默认保留。
|
||||
|
||||
**第二步**——对以下关键集用模式A模板展开详情:
|
||||
- 🔴 幕末转折集、付费卡点集、高潮集
|
||||
- 🟡 首集
|
||||
|
||||
### 全局删减决策记录
|
||||
|
||||
| 决策 | 被删/压缩内容 | 原因 |
|
||||
|------|--------------|------|
|
||||
| 删 | {具体内容} | {原因} |
|
||||
| 压缩 | {具体内容} | {原因} |
|
||||
|
||||
### 付费卡点设计
|
||||
|
||||
| 位置 | 内容 | 类型 |
|
||||
|------|------|------|
|
||||
| 集{N}末 | {卡点内容} | {智识钩子/悬念钩子/情感钩子/世界观钩子} |
|
||||
|
||||
---
|
||||
|
||||
### 自查清单(生成后内部校验,不输出)
|
||||
|
||||
- [ ] 总集数、每集时长符合【项目配置】
|
||||
- [ ] 前2集无付费点
|
||||
- [ ] 每集有集末钩子,三幕均有幕末转折
|
||||
- [ ] 删减记录与分集中的删减一致
|
||||
- [ ] 章节编号与事件表一致,无虚构章节
|
||||
Binary file not shown.
2174
data/web/index.html
2174
data/web/index.html
File diff suppressed because one or more lines are too long
154
src/agents/productionAgent/index copy.ts
Normal file
154
src/agents/productionAgent/index copy.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import { Socket } from "socket.io";
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
import u from "@/utils";
|
||||
import Memory from "@/utils/agent/memory";
|
||||
import { useSkill } from "@/utils/agent/skillsTools";
|
||||
import useTools from "@/agents/productionAgent/tools";
|
||||
import ResTool from "@/socket/resTool";
|
||||
import * as fs from "fs";
|
||||
|
||||
export interface AgentContext {
|
||||
socket: Socket;
|
||||
isolationKey: string;
|
||||
text: string;
|
||||
userMessageTime?: number;
|
||||
abortSignal?: AbortSignal;
|
||||
resTool: ResTool;
|
||||
msg: ReturnType<ResTool["newMessage"]>;
|
||||
}
|
||||
|
||||
function buildMemPrompt(mem: Awaited<ReturnType<Memory["get"]>>): string {
|
||||
let memoryContext = "";
|
||||
if (mem.rag.length) {
|
||||
memoryContext += `[相关记忆]\n${mem.rag.map((r) => r.content).join("\n")}`;
|
||||
}
|
||||
if (mem.summaries.length) {
|
||||
if (memoryContext) memoryContext += "\n\n";
|
||||
memoryContext += `[历史摘要]\n${mem.summaries.map((s, i) => `${i + 1}. ${s.content}`).join("\n")}`;
|
||||
}
|
||||
if (mem.shortTerm.length) {
|
||||
if (memoryContext) memoryContext += "\n\n";
|
||||
memoryContext += `[近期对话]\n${mem.shortTerm.map((m) => `${m.role}: ${m.content}`).join("\n")}`;
|
||||
}
|
||||
return `## Memory\n以下是你对用户的记忆,可作为参考但不要主动提及:\n${memoryContext}`;
|
||||
}
|
||||
|
||||
const subAgentList = ["executionAI", "supervisionAI"] as const;
|
||||
|
||||
export async function decisionAI(ctx: AgentContext) {
|
||||
const { isolationKey, text, abortSignal } = ctx;
|
||||
const memory = new Memory("productionAgent", isolationKey);
|
||||
await memory.add("user", text);
|
||||
|
||||
const { skillPaths } = await useSkill({ mainSkill: "production_agent_decision" });
|
||||
const prompt = await fs.promises.readFile(skillPaths.mainSkill, "utf-8");
|
||||
|
||||
const mem = buildMemPrompt(await memory.get(text));
|
||||
|
||||
const { textStream } = await u.Ai.Text("productionAgent").stream({
|
||||
messages: [
|
||||
{ role: "system", content: prompt },
|
||||
{ role: "system", content: mem },
|
||||
{ role: "user", content: text },
|
||||
],
|
||||
abortSignal,
|
||||
tools: {
|
||||
...memory.getTools(),
|
||||
run_sub_agent: runSubAgent(ctx),
|
||||
...useTools({ resTool: ctx.resTool, msg: ctx.msg }),
|
||||
},
|
||||
onFinish: async (completion) => {
|
||||
await memory.add("assistant:decision", completion.text);
|
||||
},
|
||||
});
|
||||
|
||||
return textStream;
|
||||
}
|
||||
|
||||
//====================== 执行层 ======================
|
||||
|
||||
export async function executionAI(ctx: AgentContext) {
|
||||
const { text, abortSignal } = ctx;
|
||||
|
||||
const skill = await useSkill({
|
||||
mainSkill: "production_agent_execution",
|
||||
workspace: ["production_agent_skills/execution"],
|
||||
attachedSkills: ["production_agent_skills/execution/driector_art_skills/chinese_sweet_romance/driector_skills"], //todo:后续可以改为动态加载
|
||||
});
|
||||
|
||||
const subMsg = ctx.resTool.newMessage("assistant", "执行导演");
|
||||
|
||||
const { textStream } = await u.Ai.Text("productionAgent").stream({
|
||||
system: skill.prompt,
|
||||
messages: [{ role: "user", content: text }],
|
||||
abortSignal,
|
||||
tools: {
|
||||
...skill.tools,
|
||||
...useTools({ resTool: ctx.resTool, msg: subMsg }),
|
||||
},
|
||||
});
|
||||
|
||||
return { textStream, subMsg };
|
||||
}
|
||||
|
||||
export async function supervisionAI(ctx: AgentContext) {
|
||||
const { text, abortSignal } = ctx;
|
||||
|
||||
const skill = await useSkill({ mainSkill: "production_agent_supervision", workspace: ["production_agent_skills/supervision"] });
|
||||
const subMsg = ctx.resTool.newMessage("assistant", "监制");
|
||||
|
||||
const { textStream } = await u.Ai.Text("productionAgent").stream({
|
||||
system: skill.prompt,
|
||||
messages: [{ role: "user", content: text }],
|
||||
abortSignal,
|
||||
tools: {
|
||||
...skill.tools,
|
||||
...useTools({
|
||||
resTool: ctx.resTool,
|
||||
msg: subMsg,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return { textStream, subMsg };
|
||||
}
|
||||
|
||||
//工具函数
|
||||
function runSubAgent(parentCtx: AgentContext) {
|
||||
const memory = new Memory("productionAgent", parentCtx.isolationKey);
|
||||
return tool({
|
||||
description: "启动子Agent执行独立任务。可用子Agent:executionAI, decisionAI, supervisionAI",
|
||||
inputSchema: z.object({
|
||||
agent: z.enum(["executionAI", "supervisionAI"]).describe("子Agent名称"),
|
||||
prompt: z.string().describe("交给子Agent的任务简约描述,100字以内"),
|
||||
}),
|
||||
execute: async ({ agent, prompt }) => {
|
||||
const fn = [executionAI, supervisionAI][subAgentList.indexOf(agent)];
|
||||
|
||||
// 先完成主Agent当前的消息
|
||||
parentCtx.msg.complete();
|
||||
// 子Agent用新消息回复
|
||||
const { textStream: subTextStream, subMsg } = await fn({ ...parentCtx, text: prompt });
|
||||
let text = subMsg.text();
|
||||
let fullResponse = "";
|
||||
for await (const chunk of subTextStream) {
|
||||
text.append(chunk);
|
||||
fullResponse += chunk;
|
||||
}
|
||||
text.complete();
|
||||
subMsg.complete();
|
||||
if (fullResponse.trim()) {
|
||||
await memory.add(`assistant:${agent === "executionAI" ? "execution" : "supervision"}`, fullResponse, {
|
||||
name: agent === "executionAI" ? "执行导演" : "监制",
|
||||
createTime: new Date(subMsg.datetime).getTime(),
|
||||
});
|
||||
}
|
||||
|
||||
// 为主Agent后续输出创建新消息
|
||||
parentCtx.msg = parentCtx.resTool.newMessage("assistant", "监制");
|
||||
|
||||
return fullResponse;
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -7,6 +7,7 @@ import { useSkill } from "@/utils/agent/skillsTools";
|
||||
import useTools from "@/agents/productionAgent/tools";
|
||||
import ResTool from "@/socket/resTool";
|
||||
import * as fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export interface AgentContext {
|
||||
socket: Socket;
|
||||
@ -34,15 +35,16 @@ function buildMemPrompt(mem: Awaited<ReturnType<Memory["get"]>>): string {
|
||||
return `## Memory\n以下是你对用户的记忆,可作为参考但不要主动提及:\n${memoryContext}`;
|
||||
}
|
||||
|
||||
const subAgentList = ["executionAI", "supervisionAI"] as const;
|
||||
|
||||
export async function decisionAI(ctx: AgentContext) {
|
||||
const { isolationKey, text, abortSignal } = ctx;
|
||||
const memory = new Memory("productionAgent", isolationKey);
|
||||
await memory.add("user", text);
|
||||
|
||||
const { skillPaths } = await useSkill({ mainSkill: "production_agent_decision" });
|
||||
const prompt = await fs.promises.readFile(skillPaths.mainSkill, "utf-8");
|
||||
// const { skillPaths } = await useSkill({ mainSkill: "production_agent_decision" });
|
||||
// const prompt = await fs.promises.readFile(skillPaths.mainSkill, "utf-8");
|
||||
|
||||
const skill = path.join(u.getPath("skills"), "production_agent_decision.md");
|
||||
const prompt = await fs.promises.readFile(skill, "utf-8");
|
||||
|
||||
const mem = buildMemPrompt(await memory.get(text));
|
||||
|
||||
@ -55,8 +57,8 @@ export async function decisionAI(ctx: AgentContext) {
|
||||
abortSignal,
|
||||
tools: {
|
||||
...memory.getTools(),
|
||||
run_sub_agent: runSubAgent(ctx),
|
||||
...useTools({ resTool: ctx.resTool, msg: ctx.msg }),
|
||||
...createSubAgent(ctx),
|
||||
},
|
||||
onFinish: async (completion) => {
|
||||
await memory.add("assistant:decision", completion.text);
|
||||
@ -66,89 +68,183 @@ export async function decisionAI(ctx: AgentContext) {
|
||||
return textStream;
|
||||
}
|
||||
|
||||
//====================== 执行层 ======================
|
||||
|
||||
export async function executionAI(ctx: AgentContext) {
|
||||
const { text, abortSignal } = ctx;
|
||||
|
||||
const skill = await useSkill({
|
||||
mainSkill: "production_agent_execution",
|
||||
workspace: ["production_agent_skills/execution"],
|
||||
attachedSkills: ["production_agent_skills/execution/driector_art_skills/chinese_sweet_romance/driector_skills"], //todo:后续可以改为动态加载
|
||||
});
|
||||
|
||||
const subMsg = ctx.resTool.newMessage("assistant", "执行导演");
|
||||
|
||||
const { textStream } = await u.Ai.Text("productionAgent").stream({
|
||||
system: skill.prompt,
|
||||
messages: [{ role: "user", content: text }],
|
||||
abortSignal,
|
||||
tools: {
|
||||
...skill.tools,
|
||||
...useTools({ resTool: ctx.resTool, msg: subMsg }),
|
||||
},
|
||||
});
|
||||
|
||||
return { textStream, subMsg };
|
||||
}
|
||||
|
||||
export async function supervisionAI(ctx: AgentContext) {
|
||||
const { text, abortSignal } = ctx;
|
||||
|
||||
const skill = await useSkill({ mainSkill: "production_agent_supervision", workspace: ["production_agent_skills/supervision"] });
|
||||
const subMsg = ctx.resTool.newMessage("assistant", "监制");
|
||||
|
||||
const { textStream } = await u.Ai.Text("productionAgent").stream({
|
||||
system: skill.prompt,
|
||||
messages: [{ role: "user", content: text }],
|
||||
abortSignal,
|
||||
tools: {
|
||||
...skill.tools,
|
||||
...useTools({
|
||||
resTool: ctx.resTool,
|
||||
msg: subMsg,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return { textStream, subMsg };
|
||||
}
|
||||
|
||||
//工具函数
|
||||
function runSubAgent(parentCtx: AgentContext) {
|
||||
function createSubAgent(parentCtx: AgentContext) {
|
||||
const { resTool, abortSignal } = parentCtx;
|
||||
const memory = new Memory("productionAgent", parentCtx.isolationKey);
|
||||
return tool({
|
||||
description: "启动子Agent执行独立任务。可用子Agent:executionAI, decisionAI, supervisionAI",
|
||||
inputSchema: z.object({
|
||||
agent: z.enum(["executionAI", "supervisionAI"]).describe("子Agent名称"),
|
||||
prompt: z.string().describe("交给子Agent的任务简约描述,100字以内"),
|
||||
}),
|
||||
execute: async ({ agent, prompt }) => {
|
||||
const fn = [executionAI, supervisionAI][subAgentList.indexOf(agent)];
|
||||
|
||||
// 先完成主Agent当前的消息
|
||||
async function runAgent({
|
||||
prompt,
|
||||
system,
|
||||
name,
|
||||
memoryKey,
|
||||
tools: extraTools,
|
||||
}: {
|
||||
prompt: string;
|
||||
system: string;
|
||||
name: string;
|
||||
memoryKey: string;
|
||||
tools?: Record<string, any>;
|
||||
}) {
|
||||
parentCtx.msg.complete();
|
||||
// 子Agent用新消息回复
|
||||
const { textStream: subTextStream, subMsg } = await fn({ ...parentCtx, text: prompt });
|
||||
let text = subMsg.text();
|
||||
const subMsg = resTool.newMessage("assistant", name);
|
||||
const text = subMsg.text();
|
||||
let fullResponse = "";
|
||||
for await (const chunk of subTextStream) {
|
||||
|
||||
const { textStream } = await u.Ai.Text("scriptAgent").stream({
|
||||
system,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
abortSignal,
|
||||
tools: { ...extraTools, ...useTools({ resTool, msg: subMsg }) },
|
||||
});
|
||||
|
||||
for await (const chunk of textStream) {
|
||||
text.append(chunk);
|
||||
fullResponse += chunk;
|
||||
}
|
||||
|
||||
text.complete();
|
||||
subMsg.complete();
|
||||
|
||||
if (fullResponse.trim()) {
|
||||
await memory.add(`assistant:${agent === "executionAI" ? "execution" : "supervision"}`, fullResponse, {
|
||||
name: agent === "executionAI" ? "执行导演" : "监制",
|
||||
await memory.add(memoryKey, fullResponse, {
|
||||
name,
|
||||
createTime: new Date(subMsg.datetime).getTime(),
|
||||
});
|
||||
}
|
||||
|
||||
// 为主Agent后续输出创建新消息
|
||||
parentCtx.msg = parentCtx.resTool.newMessage("assistant", "监制");
|
||||
|
||||
parentCtx.msg = resTool.newMessage("assistant", "视频策划");
|
||||
return fullResponse;
|
||||
}
|
||||
|
||||
const promptInput = z.object({
|
||||
prompt: z.string().describe("交给子Agent的任务简约描述,100字以内"),
|
||||
});
|
||||
|
||||
const run_sub_agent_execution = tool({
|
||||
description: "执行层子Agent,负责衍生资产、",
|
||||
inputSchema: promptInput,
|
||||
execute: async ({ prompt }) => {
|
||||
const skill = path.join(u.getPath("skills"), "production_agent_execution.md");
|
||||
const systemPrompt = await fs.promises.readFile(skill, "utf-8");
|
||||
const addPrompt =
|
||||
"\n" +
|
||||
[
|
||||
"你可以使用如下XML格式写入工作区:\n```",
|
||||
"剧本:<script>内容</script>",
|
||||
"拍摄计划:<scriptPlan>内容</scriptPlan>",
|
||||
"分镜表:<storyboardTable>内容</storyboardTable>",
|
||||
"```",
|
||||
].join("\n");
|
||||
|
||||
return runAgent({
|
||||
prompt,
|
||||
system: systemPrompt + addPrompt,
|
||||
name: "执行导演",
|
||||
memoryKey: "assistant:execution",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const run_sub_agent_supervision = tool({
|
||||
description: "监制层子Agent,负责审核执行结果",
|
||||
inputSchema: promptInput,
|
||||
execute: async ({ prompt }) => {
|
||||
const skill = path.join(u.getPath("skills"), "production_agent_supervision.md");
|
||||
const systemPrompt = await fs.promises.readFile(skill, "utf-8");
|
||||
return runAgent({
|
||||
prompt,
|
||||
system: systemPrompt + "你可以使用如下XML格式写入工作区:\n<storySkeleton>故事骨架内容</storySkeleton>",
|
||||
name: "监制",
|
||||
memoryKey: "assistant:supervision",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return { run_sub_agent_execution, run_sub_agent_supervision };
|
||||
}
|
||||
|
||||
// //====================== 执行层 ======================
|
||||
|
||||
// export async function executionAI(ctx: AgentContext) {
|
||||
// const { text, abortSignal } = ctx;
|
||||
|
||||
// const skill = await useSkill({
|
||||
// mainSkill: "production_agent_execution",
|
||||
// workspace: ["production_agent_skills/execution"],
|
||||
// attachedSkills: ["production_agent_skills/execution/driector_art_skills/chinese_sweet_romance/driector_skills"], //todo:后续可以改为动态加载
|
||||
// });
|
||||
|
||||
// const subMsg = ctx.resTool.newMessage("assistant", "执行导演");
|
||||
|
||||
// const { textStream } = await u.Ai.Text("productionAgent").stream({
|
||||
// system: skill.prompt,
|
||||
// messages: [{ role: "user", content: text }],
|
||||
// abortSignal,
|
||||
// tools: {
|
||||
// ...skill.tools,
|
||||
// ...useTools({ resTool: ctx.resTool, msg: subMsg }),
|
||||
// },
|
||||
// });
|
||||
|
||||
// return { textStream, subMsg };
|
||||
// }
|
||||
|
||||
// export async function supervisionAI(ctx: AgentContext) {
|
||||
// const { text, abortSignal } = ctx;
|
||||
|
||||
// const skill = await useSkill({ mainSkill: "production_agent_supervision", workspace: ["production_agent_skills/supervision"] });
|
||||
// const subMsg = ctx.resTool.newMessage("assistant", "监制");
|
||||
|
||||
// const { textStream } = await u.Ai.Text("productionAgent").stream({
|
||||
// system: skill.prompt,
|
||||
// messages: [{ role: "user", content: text }],
|
||||
// abortSignal,
|
||||
// tools: {
|
||||
// ...skill.tools,
|
||||
// ...useTools({
|
||||
// resTool: ctx.resTool,
|
||||
// msg: subMsg,
|
||||
// }),
|
||||
// },
|
||||
// });
|
||||
|
||||
// return { textStream, subMsg };
|
||||
// }
|
||||
|
||||
// //工具函数
|
||||
// function runSubAgent(parentCtx: AgentContext) {
|
||||
// const memory = new Memory("productionAgent", parentCtx.isolationKey);
|
||||
// return tool({
|
||||
// description: "启动子Agent执行独立任务。可用子Agent:executionAI, decisionAI, supervisionAI",
|
||||
// inputSchema: z.object({
|
||||
// agent: z.enum(["executionAI", "supervisionAI"]).describe("子Agent名称"),
|
||||
// prompt: z.string().describe("交给子Agent的任务简约描述,100字以内"),
|
||||
// }),
|
||||
// execute: async ({ agent, prompt }) => {
|
||||
// const fn = [executionAI, supervisionAI][subAgentList.indexOf(agent)];
|
||||
|
||||
// // 先完成主Agent当前的消息
|
||||
// parentCtx.msg.complete();
|
||||
// // 子Agent用新消息回复
|
||||
// const { textStream: subTextStream, subMsg } = await fn({ ...parentCtx, text: prompt });
|
||||
// let text = subMsg.text();
|
||||
// let fullResponse = "";
|
||||
// for await (const chunk of subTextStream) {
|
||||
// text.append(chunk);
|
||||
// fullResponse += chunk;
|
||||
// }
|
||||
// text.complete();
|
||||
// subMsg.complete();
|
||||
// if (fullResponse.trim()) {
|
||||
// await memory.add(`assistant:${agent === "executionAI" ? "execution" : "supervision"}`, fullResponse, {
|
||||
// name: agent === "executionAI" ? "执行导演" : "监制",
|
||||
// createTime: new Date(subMsg.datetime).getTime(),
|
||||
// });
|
||||
// }
|
||||
|
||||
// // 为主Agent后续输出创建新消息
|
||||
// parentCtx.msg = parentCtx.resTool.newMessage("assistant", "监制");
|
||||
|
||||
// return fullResponse;
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
751
src/agents/productionAgent/tools copy.ts
Normal file
751
src/agents/productionAgent/tools copy.ts
Normal file
@ -0,0 +1,751 @@
|
||||
import { tool, Tool } from "ai";
|
||||
import { z } from "zod";
|
||||
import _ from "lodash";
|
||||
import ResTool from "@/socket/resTool";
|
||||
import u from "@/utils";
|
||||
import { urlToBase64 } from "@/utils/vm";
|
||||
export const deriveAssetSchema = z.object({
|
||||
id: z.number().describe("衍生资产ID,如果新增则为空"),
|
||||
assetsId: z.number().describe("关联的资产ID"),
|
||||
prompt: z.string().describe("生成提示词"),
|
||||
name: z.string().describe("衍生资产名称"),
|
||||
desc: z.string().describe("衍生资产描述"),
|
||||
src: z.string().nullable().describe("衍生资产资源路径"),
|
||||
state: z.enum(["未生成", "生成中", "已完成", "生成失败"]).describe("衍生资产生成状态"),
|
||||
type: z.enum(["role", "tool", "scene", "clip"]).describe("衍生资产类型"),
|
||||
});
|
||||
export const assetItemSchema = z.object({
|
||||
id: z.number().describe("资产唯一标识"),
|
||||
name: z.string().describe("资产名称"),
|
||||
type: z.enum(["role", "tool", "scene", "clip"]).describe("资产类型"),
|
||||
prompt: z.string().describe("生成提示词"),
|
||||
desc: z.string().describe("资产描述"),
|
||||
derive: z.array(deriveAssetSchema).describe("衍生资产列表"),
|
||||
});
|
||||
export const storyboardSchema = z.object({
|
||||
id: z.number().describe("分镜ID,必须为真实id"),
|
||||
title: z.string().describe("分镜标题"),
|
||||
description: z.string().describe("分镜描述"),
|
||||
camera: z.string().describe("镜头信息"),
|
||||
duration: z.number().describe("持续时长(秒)"),
|
||||
frameMode: z.enum(["firstFrame", "endFrame", "linesSoundEffects"]).describe("帧模式: 首帧/尾帧/台词音效"),
|
||||
prompt: z.string().describe("生成提示词"),
|
||||
lines: z.string().nullable().describe("台词内容"),
|
||||
sound: z.string().nullable().describe("音效内容"),
|
||||
mode: z
|
||||
.union([
|
||||
z.enum(["singleImage", "multiImage", "gridImage", "startEndRequired", "endFrameOptional", "startFrameOptional", "text"]),
|
||||
z.array(z.enum(["video", "image", "audio", "text"])),
|
||||
])
|
||||
.describe("视频模式"),
|
||||
associateAssetsIds: z.array(z.number()).describe("关联资产ID列表"),
|
||||
src: z.string().nullable().describe("分镜资源路径"),
|
||||
});
|
||||
export const workbenchDataSchema = z.object({
|
||||
name: z.string().describe("项目名称"),
|
||||
duration: z.string().describe("视频时长"),
|
||||
resolution: z.string().describe("分辨率"),
|
||||
fps: z.string().describe("帧率"),
|
||||
cover: z.string().optional().describe("封面图片路径"),
|
||||
gradient: z.string().optional().describe("渐变色配置"),
|
||||
});
|
||||
export const posterItemSchema = z.object({
|
||||
id: z.number().describe("海报ID"),
|
||||
image: z.string().describe("海报图片路径"),
|
||||
});
|
||||
export const flowDataSchema = z.object({
|
||||
script: z.string().describe("剧本内容"),
|
||||
scriptPlan: z.string().describe("拍摄计划"),
|
||||
assets: z.array(assetItemSchema).describe("衍生资产"),
|
||||
storyboardTable: z.string().describe("分镜表"),
|
||||
storyboard: z.array(storyboardSchema).describe("分镜面板"),
|
||||
workbench: workbenchDataSchema.describe("工作台配置"),
|
||||
poster: z
|
||||
.object({
|
||||
items: z.array(posterItemSchema).describe("海报项目列表"),
|
||||
})
|
||||
.describe("海报配置"),
|
||||
});
|
||||
|
||||
export type FlowData = z.infer<typeof flowDataSchema>;
|
||||
|
||||
const keySchema = z.enum(Object.keys(flowDataSchema.shape) as [keyof FlowData, ...Array<keyof FlowData>]);
|
||||
const flowDataKeyLabels = Object.fromEntries(
|
||||
Object.entries(flowDataSchema.shape).map(([key, schema]) => [key, (schema as z.ZodTypeAny).description ?? key]),
|
||||
) as Record<keyof FlowData, string>;
|
||||
|
||||
interface ToolConfig {
|
||||
resTool: ResTool;
|
||||
toolsNames?: string[];
|
||||
msg: ReturnType<ResTool["newMessage"]>;
|
||||
}
|
||||
|
||||
export default (toolCpnfig: ToolConfig) => {
|
||||
const { resTool, toolsNames, msg } = toolCpnfig;
|
||||
const { socket } = resTool;
|
||||
const tools: Record<string, Tool> = {
|
||||
get_flowData: tool({
|
||||
description: "获取工作区数据",
|
||||
inputSchema: z.object({
|
||||
key: keySchema.describe("数据key"),
|
||||
}),
|
||||
execute: async ({ key }) => {
|
||||
const thinking = msg.thinking(`正在获取${flowDataKeyLabels[key]}工作区数据...`);
|
||||
console.log("[tools] get_flowData", key);
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key }, (res: any) => resolve(res)));
|
||||
thinking.appendText(`获取到${flowDataKeyLabels[key]}:\n` + flowData[key]);
|
||||
thinking.updateTitle(`获取${flowDataKeyLabels[key]}完成`);
|
||||
thinking.complete();
|
||||
return flowData[key];
|
||||
},
|
||||
}),
|
||||
set_flowData_script: tool({
|
||||
description: "保存剧本内容到工作区",
|
||||
inputSchema: z.object({ value: flowDataSchema.shape.script }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_flowData script", value);
|
||||
const thinking = msg.thinking("正在保存 剧本 数据");
|
||||
socket.emit("setFlowData", { key: "script", value });
|
||||
thinking.updateTitle("保存 剧本 数据完成");
|
||||
thinking.complete();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
set_flowData_scriptPlan: tool({
|
||||
description: "保存拍摄计划到工作区",
|
||||
inputSchema: z.object({ value: flowDataSchema.shape.scriptPlan }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_flowData scriptPlan", value);
|
||||
const thinking = msg.thinking("正在保存 拍摄计划 数据");
|
||||
socket.emit("setFlowData", { key: "scriptPlan", value });
|
||||
thinking.updateTitle("保存 拍摄计划 数据完成");
|
||||
thinking.complete();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
add_flowData_assets: tool({
|
||||
description: "新增对应衍生资产列表到工作区,严禁包含 不需要新增的数据",
|
||||
inputSchema: z.object({ value: z.array(deriveAssetSchema.omit({ id: true })).describe("需要新增的衍生资产列表") }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_flowData add_flowData_assets", value);
|
||||
const thinking = msg.thinking("正在保存 衍生资产 数据");
|
||||
const setData = [...value] as z.infer<typeof deriveAssetSchema>[];
|
||||
const { projectId, scriptId } = resTool.data;
|
||||
const startTime = Date.now();
|
||||
|
||||
// 并行插入所有 o_assets 记录
|
||||
await Promise.all(
|
||||
setData.map(async (i) => {
|
||||
const [insertedId] = await u.db("o_assets").insert({
|
||||
assetsId: +i.assetsId || null,
|
||||
projectId,
|
||||
name: i.name,
|
||||
type: i.type,
|
||||
prompt: i.prompt,
|
||||
describe: i.desc,
|
||||
startTime,
|
||||
});
|
||||
i.id = insertedId;
|
||||
}),
|
||||
);
|
||||
|
||||
// 批量插入 o_scriptAssets
|
||||
await u.db("o_scriptAssets").insert(setData.map((i) => ({ scriptId, assetId: i.id })));
|
||||
|
||||
const watiAddAssetsMap: Record<number, z.infer<typeof deriveAssetSchema>[]> = {};
|
||||
setData.forEach((i) => {
|
||||
if (watiAddAssetsMap[i.assetsId]) {
|
||||
watiAddAssetsMap[i.assetsId].push(i);
|
||||
} else {
|
||||
watiAddAssetsMap[i.assetsId] = [i];
|
||||
}
|
||||
});
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "assets" }, (res: any) => resolve(res)));
|
||||
const assetsData = flowData.assets;
|
||||
assetsData.forEach((i) => {
|
||||
if (watiAddAssetsMap[i.id]) {
|
||||
i.derive = [...(i.derive || []), ...watiAddAssetsMap[i.id]];
|
||||
}
|
||||
});
|
||||
thinking.updateTitle("保存 衍生资产 数据完成");
|
||||
thinking.complete();
|
||||
|
||||
socket.emit("setFlowData", { key: "assets", value: assetsData });
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
update_flowData_assets: tool({
|
||||
description: "更新对应衍生资产列表到工作区",
|
||||
inputSchema: z.object({ value: z.array(deriveAssetSchema).describe("需要更新的衍生资产列表") }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] update_flowData update_flowData_assets", value);
|
||||
const thinking = msg.thinking("正在保存 衍生资产 数据");
|
||||
for (const i of value) {
|
||||
await u
|
||||
.db("o_assets")
|
||||
.where("id", i.id)
|
||||
.update({
|
||||
assetsId: +i.assetsId || null,
|
||||
projectId: resTool.data.projectId,
|
||||
name: i.name,
|
||||
type: i.type,
|
||||
prompt: i.prompt,
|
||||
describe: i.desc,
|
||||
});
|
||||
}
|
||||
// 按 assetsId 分组,构建更新映射
|
||||
const updateAssetsMap: Record<number, z.infer<typeof deriveAssetSchema>[]> = {};
|
||||
value.forEach((i) => {
|
||||
if (updateAssetsMap[i.assetsId]) {
|
||||
updateAssetsMap[i.assetsId].push(i);
|
||||
} else {
|
||||
updateAssetsMap[i.assetsId] = [i];
|
||||
}
|
||||
});
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "assets" }, (res: any) => resolve(res)));
|
||||
const assetsData = flowData.assets;
|
||||
// 将 derive 中已存在的条目替换为更新后的数据
|
||||
assetsData.forEach((asset) => {
|
||||
if (updateAssetsMap[asset.id]) {
|
||||
const updatedMap = Object.fromEntries(updateAssetsMap[asset.id].map((d) => [d.id, d]));
|
||||
asset.derive = (asset.derive || []).map((d) => updatedMap[d.id] ?? d);
|
||||
}
|
||||
});
|
||||
thinking.updateTitle("保存 衍生资产 数据完成");
|
||||
thinking.complete();
|
||||
socket.emit("setFlowData", { key: "assets", value: assetsData });
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
delete_flowData_assets: tool({
|
||||
description: "删除对应衍生资产",
|
||||
inputSchema: z.object({ ids: z.array(z.number()).describe("需要删除的 衍生资产id ") }),
|
||||
execute: async ({ ids }) => {
|
||||
console.log("[tools] delete_flowData delete_flowData_assets", ids);
|
||||
const thinking = msg.thinking("正在删除指定 衍生资产 数据...");
|
||||
await u.db("o_assets").whereIn("id", ids).delete();
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "assets" }, (res: any) => resolve(res)));
|
||||
const assetsData = flowData.assets;
|
||||
assetsData.forEach((i) => {
|
||||
i.derive = (i.derive || []).filter((d) => !ids.includes(d.id));
|
||||
});
|
||||
thinking.updateTitle("删除指定 衍生资产 数据完成");
|
||||
thinking.complete();
|
||||
// 将 derive 中已存在的条目替换为更新后的数据
|
||||
socket.emit("setFlowData", { key: "assets", value: assetsData });
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
// set_flowData_assets: tool({
|
||||
// description: "保存衍生资产列表到工作区",
|
||||
// inputSchema: z.object({ value: flowDataSchema.shape.assets }),
|
||||
// execute: async ({ value }) => {
|
||||
// console.log("[tools] set_flowData assets", value);
|
||||
// resTool.systemMessage("正在保存 衍生资产 数据");
|
||||
// if (value && Array.isArray(value) && value.length) {
|
||||
// for (const i of value) {
|
||||
// if (!i?.id) {
|
||||
// const [insertedId] = await u.db("o_assets").insert({
|
||||
// assetsId: null,
|
||||
// name: i.name,
|
||||
// type: i.type,
|
||||
// prompt: i.prompt,
|
||||
// describe: i.desc,
|
||||
// startTime: Date.now(),
|
||||
// });
|
||||
// i.id = insertedId;
|
||||
// }
|
||||
// if (i.derive && Array.isArray(i.derive) && i.derive.length) {
|
||||
// for (const sub of i.derive) {
|
||||
// if (sub.id) continue;
|
||||
// const [insertedId] = await u.db("o_assets").insert({
|
||||
// assetsId: +i.id || null,
|
||||
// projectId: resTool.data.projectId,
|
||||
// name: sub.name,
|
||||
// type: sub.type,
|
||||
// prompt: sub.prompt,
|
||||
// describe: sub.desc,
|
||||
// startTime: Date.now(),
|
||||
// });
|
||||
// await u.db("o_scriptAssets").insert({
|
||||
// scriptId: resTool.data.scriptId,
|
||||
// assetId: insertedId,
|
||||
// });
|
||||
// sub.id = insertedId;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// socket.emit("setFlowData", { key: "assets", value });
|
||||
// return true;
|
||||
// },
|
||||
// }),
|
||||
set_flowData_storyboardTable: tool({
|
||||
description: "保存分镜表到工作区",
|
||||
inputSchema: z.object({ value: flowDataSchema.shape.storyboardTable }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_flowData storyboardTable", value);
|
||||
const thinking = msg.thinking("正在保存 分镜表 数据...");
|
||||
socket.emit("setFlowData", { key: "storyboardTable", value });
|
||||
thinking.updateTitle("保存 分镜表 数据完成");
|
||||
thinking.complete();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
add_flowData_storyboard: tool({
|
||||
description: "新增分镜面板到工作区",
|
||||
inputSchema: z.object({ value: z.array(storyboardSchema.omit({ id: true })) }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] add_flowData storyboard", value);
|
||||
const thinking = msg.thinking("正在保存 分镜面板 数据...");
|
||||
const setData = [...value] as z.infer<typeof storyboardSchema>[];
|
||||
for (const item of setData) {
|
||||
item.src = "";
|
||||
const [insertedId] = await u.db("o_storyboard").insert({
|
||||
title: item.title,
|
||||
prompt: item.prompt,
|
||||
description: item.description,
|
||||
frameMode: item.frameMode,
|
||||
duration: String(item.duration),
|
||||
camera: item.camera,
|
||||
sound: item.sound,
|
||||
lines: item.lines,
|
||||
state: "未生成",
|
||||
scriptId: resTool.data.scriptId,
|
||||
createTime: Date.now(),
|
||||
});
|
||||
if (item.associateAssetsIds.length) {
|
||||
await u.db("o_assets2Storyboard").insert(item.associateAssetsIds.map((i) => ({ storyboardId: insertedId, assetId: i })));
|
||||
}
|
||||
item.id = insertedId;
|
||||
}
|
||||
//为了防止丢失分镜其他数据,例如:依赖分镜Id、依赖资产idc
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "storyboard" }, (res: any) => resolve(res)));
|
||||
const storyboardData = flowData["storyboard"].concat([...setData]);
|
||||
socket.emit("setFlowData", { key: "storyboard", value: storyboardData });
|
||||
|
||||
thinking.updateTitle("保存 分镜面板 数据完成");
|
||||
thinking.complete();
|
||||
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
update_flowData_storyboard: tool({
|
||||
description: "更新指定分镜面板到工作区",
|
||||
inputSchema: z.object({ value: flowDataSchema.shape.storyboard }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] update_flowData storyboard", value);
|
||||
const thinking = msg.thinking("正在保存 分镜面板 数据...");
|
||||
for (const item of value) {
|
||||
await u
|
||||
.db("o_storyboard")
|
||||
.where("id", item.id)
|
||||
.update({
|
||||
title: item.title,
|
||||
prompt: item.prompt,
|
||||
description: item.description,
|
||||
frameMode: item.frameMode,
|
||||
duration: String(item.duration),
|
||||
camera: item.camera,
|
||||
sound: item.sound,
|
||||
lines: item.lines,
|
||||
});
|
||||
}
|
||||
//直接拉取前端数据,为了防止丢失分镜其他数据,例如:依赖分镜Id、依赖资产idc
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "storyboard" }, (res: any) => resolve(res)));
|
||||
const storyboardData = flowData["storyboard"].map((existing) => {
|
||||
const updated = value.find((v) => v.id === existing.id);
|
||||
if (!updated) return existing;
|
||||
return {
|
||||
...existing,
|
||||
title: updated.title,
|
||||
prompt: updated.prompt,
|
||||
description: updated.description,
|
||||
frameMode: updated.frameMode,
|
||||
duration: updated.duration,
|
||||
camera: updated.camera,
|
||||
sound: updated.sound,
|
||||
lines: updated.lines,
|
||||
};
|
||||
});
|
||||
socket.emit("setFlowData", { key: "storyboard", value: storyboardData });
|
||||
thinking.updateTitle("保存 分镜面板 数据完成");
|
||||
thinking.complete();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
delete_flowData_storyboard: tool({
|
||||
description: "删除指定分镜面板并更新工作区",
|
||||
inputSchema: z.object({ ids: z.array(z.number()).describe("需要删除的 分镜id ") }),
|
||||
execute: async ({ ids }) => {
|
||||
console.log("[tools] delete_flowData storyboard", ids);
|
||||
const thinking = msg.thinking("正在删除指定 分镜面板 数据...");
|
||||
await u.db("o_storyboard").whereIn("id", ids).delete();
|
||||
await u.db("o_assets2Storyboard").whereIn("storyboardId", ids).delete();
|
||||
await u.db("o_storyboardFlow").whereIn("storyboardId", ids).delete();
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "storyboard" }, (res: any) => resolve(res)));
|
||||
const storyboardData = flowData["storyboard"].filter((item) => !ids.includes(item.id));
|
||||
socket.emit("setFlowData", { key: "storyboard", value: storyboardData });
|
||||
thinking.updateTitle("删除指定 分镜面板 数据完成");
|
||||
thinking.complete();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
// set_flowData_storyboard: tool({
|
||||
// description: "保存分镜面板到工作区",
|
||||
// inputSchema: z.object({ value: flowDataSchema.shape.storyboard }),
|
||||
// execute: async ({ value }) => {
|
||||
// console.log("[tools] set_flowData storyboard", value);
|
||||
// resTool.systemMessage("正在保存 分镜面板 数据...");
|
||||
// for (const item of value) {
|
||||
// if (!item.id) {
|
||||
// const [insertedId] = await u.db("o_storyboard").insert({
|
||||
// title: item.title,
|
||||
// prompt: item.prompt,
|
||||
// description: item.description,
|
||||
// filePath: item.src,
|
||||
// frameMode: item.frameMode,
|
||||
// duration: String(item.duration),
|
||||
// camera: item.camera,
|
||||
// sound: item.sound,
|
||||
// lines: item.lines,
|
||||
// state: "未生成",
|
||||
// scriptId: resTool.data.scriptId,
|
||||
// });
|
||||
// console.log("%c Line:216 🥥 item.associateAssetsIds", "background:#6ec1c2", item.associateAssetsIds);
|
||||
|
||||
// if (item.associateAssetsIds.length) {
|
||||
// await u.db("o_assets2Storyboard").insert(item.associateAssetsIds.map((i) => ({ storyboardId: insertedId, assetId: i })));
|
||||
// }
|
||||
// item.id = insertedId;
|
||||
// }
|
||||
// }
|
||||
// socket.emit("setFlowData", { key: "storyboard", value });
|
||||
// return true;
|
||||
// },
|
||||
// }),
|
||||
set_flowData_workbench: tool({
|
||||
description: "保存工作台配置数据到工作区",
|
||||
inputSchema: z.object({ value: flowDataSchema.shape.workbench }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_flowData workbench", value);
|
||||
const thinking = msg.thinking("正在保存 工作台配置 数据...");
|
||||
socket.emit("setFlowData", { key: "workbench", value });
|
||||
thinking.updateTitle("保存 工作台配置 数据完成");
|
||||
thinking.complete();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
set_flowData_poster: tool({
|
||||
description: "保存海报配置到工作区",
|
||||
inputSchema: z.object({ value: flowDataSchema.shape.poster }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_flowData poster", value);
|
||||
const thinking = msg.thinking("正在保存 海报配置 数据...");
|
||||
thinking.updateTitle("保存 海报配置 数据完成");
|
||||
thinking.complete();
|
||||
socket.emit("setFlowData", { key: "poster", value });
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
// todo 提示词待调
|
||||
generate_storyboard_images: tool({
|
||||
description: `生成一组图片任务,支持图片间的依赖关系(以图生图),基于有向无环图(DAG)拓扑排序执行。
|
||||
|
||||
参数说明:
|
||||
- images: 图片任务数组
|
||||
- id: 图片唯一标识符(分镜id)
|
||||
- prompt: 图片生成提示词
|
||||
- referenceIds: 依赖的参考图id数组,无依赖填空数组[]
|
||||
- assetIds: 参考的资产图id数组(可选)
|
||||
|
||||
依赖规则:
|
||||
1. referenceIds中的id必须存在于images数组中
|
||||
2. 禁止循环依赖(如A依赖B,B依赖A)
|
||||
3. 被依赖的图片会先生成,其结果作为参考图传入
|
||||
|
||||
示例:生成猫图,再以猫图为参考生成狗图
|
||||
images: [
|
||||
{id: 1, prompt: "一只橘猫", referenceIds: [], assetIds: []},
|
||||
{id: 2, prompt: "风格相同的金毛犬", referenceIds: [1], assetIds: []}
|
||||
]`,
|
||||
inputSchema: z.object({
|
||||
images: z.array(
|
||||
z.object({
|
||||
id: z.number().describe("从工作区获取到的分镜id"),
|
||||
prompt: z.string().describe("图片生成提示词"),
|
||||
referenceIds: z.array(z.number()).describe("依赖的参考 分镜图id数组,无依赖填空数组[]"),
|
||||
assetIds: z.array(z.number()).describe("参考的资产图"),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
execute: async ({ images }) => {
|
||||
console.log("[tools] generate_storyboard_images", images);
|
||||
const thinking = msg.thinking("正在生成 分镜图片 数据...");
|
||||
// --- 构建任务id集合 ---
|
||||
const taskIds = new Set(images.map((item) => item.id));
|
||||
const imageMap = new Map(images.map((item) => [item.id, item]));
|
||||
|
||||
// --- 检测循环依赖 (Kahn算法拓扑排序) ---
|
||||
// 将 referenceIds 分为:本批次内依赖 vs 外部已有依赖
|
||||
// 只有本批次内的依赖才参与 DAG 调度,外部依赖直接从数据库获取
|
||||
const inDegree = new Map<number, number>();
|
||||
// adjacency: 被依赖者 -> 依赖它的节点列表
|
||||
const adjacency = new Map<number, number[]>();
|
||||
|
||||
for (const item of images) {
|
||||
// 只统计本批次内的依赖作为入度
|
||||
const internalDeps = item.referenceIds.filter((refId) => taskIds.has(refId));
|
||||
inDegree.set(item.id, internalDeps.length);
|
||||
for (const depId of internalDeps) {
|
||||
if (!adjacency.has(depId)) adjacency.set(depId, []);
|
||||
adjacency.get(depId)!.push(item.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 拓扑排序,按层级分组(同层可并行)
|
||||
const levels: number[][] = [];
|
||||
let queue = images.filter((item) => (inDegree.get(item.id) ?? 0) === 0).map((item) => item.id);
|
||||
|
||||
const visited = new Set<number>();
|
||||
while (queue.length > 0) {
|
||||
levels.push([...queue]);
|
||||
const nextQueue: number[] = [];
|
||||
for (const nodeId of queue) {
|
||||
visited.add(nodeId);
|
||||
for (const childId of adjacency.get(nodeId) ?? []) {
|
||||
inDegree.set(childId, (inDegree.get(childId) ?? 1) - 1);
|
||||
if (inDegree.get(childId) === 0) {
|
||||
nextQueue.push(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
queue = nextQueue;
|
||||
}
|
||||
// 循环依赖检测
|
||||
if (visited.size !== images.length) {
|
||||
const cyclicIds = images.filter((item) => !visited.has(item.id)).map((item) => item.id);
|
||||
thinking.appendText(`检测到循环依赖,涉及分镜id: ${cyclicIds.join(", ")},请修正后重试`);
|
||||
thinking.updateTitle("循环依赖错误");
|
||||
thinking.error();
|
||||
return `错误:检测到循环依赖,涉及分镜id: ${cyclicIds.join(", ")}`;
|
||||
}
|
||||
|
||||
thinking.appendText(`图片生成调度计划:共 ${levels.length} 层,${images.length} 张图片`);
|
||||
|
||||
// --- 准备公共数据 ---
|
||||
const projectData = await u.db("o_project").where("id", resTool.data.projectId).select("videoRatio").first();
|
||||
const imageModelData = await u.db("o_project").where("id", resTool.data.projectId).select("imageModel", "imageQuality").first();
|
||||
|
||||
// 生成单张图片的函数
|
||||
const generateOneImage = async (item: (typeof images)[0]) => {
|
||||
const thinking = msg.thinking(`正在生成分镜 id:${item.id} 图片`);
|
||||
// 更新数据库状态为生成中
|
||||
await u.db("o_storyboard").where("id", item.id).update({ state: "生成中" });
|
||||
// 更新前端为生成中
|
||||
socket.emit("setFlowData", {
|
||||
key: "setStoryboardImage",
|
||||
value: { ...item, id: item.id, src: "", state: "生成中", referenceIds: item.referenceIds },
|
||||
});
|
||||
|
||||
// 获取参考图base64(包括资产图和已生成的分镜参考图)
|
||||
const [assetsBase64, referenceBase64] = await Promise.all([
|
||||
getAssetsImageBase64(item.assetIds ?? []),
|
||||
getStoryboardImageBase64(item.referenceIds),
|
||||
]);
|
||||
|
||||
const imageCls = await u.Ai.Image(imageModelData.imageModel).run({
|
||||
prompt: item.prompt,
|
||||
imageBase64: [...assetsBase64, ...referenceBase64],
|
||||
size: imageModelData.imageQuality,
|
||||
aspectRatio: (projectData?.videoRatio as `${number}:${number}`) ?? "16:9",
|
||||
taskClass: "生成图片",
|
||||
describe: "分镜图片生成",
|
||||
relatedObjects: "hhhh",
|
||||
projectId: resTool.data.projectId,
|
||||
});
|
||||
|
||||
const savePath = `/${resTool.data.projectId}/storyboard/${u.uuid()}.jpg`;
|
||||
await imageCls.save(savePath);
|
||||
|
||||
// 更新数据库状态为已完成
|
||||
await u.db("o_storyboard").where("id", item.id).update({ state: "已完成", filePath: savePath });
|
||||
|
||||
const obj = {
|
||||
...item,
|
||||
id: item.id,
|
||||
src: await u.oss.getFileUrl(savePath),
|
||||
state: "已完成",
|
||||
referenceIds: item.referenceIds,
|
||||
};
|
||||
// 前端对话框提示
|
||||
thinking.appendText(`分镜 id:${item.id} 图片生成完成`);
|
||||
thinking.complete();
|
||||
// 更新前端界面展示
|
||||
socket.emit("setFlowData", { key: "setStoryboardImage", value: obj });
|
||||
};
|
||||
|
||||
// --- 按层级顺序执行:同层并行,层间串行 ---
|
||||
for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
|
||||
const levelIds = levels[levelIndex];
|
||||
const levelItems = levelIds.map((id) => imageMap.get(id)!);
|
||||
const thinking = msg.thinking(
|
||||
`开始生成第 ${levelIndex + 1}/${levels.length} 层,共 ${levelItems.length} 张图片 (ids: ${levelIds.join(", ")})`,
|
||||
);
|
||||
|
||||
// 同层内所有图片并行生成,使用 allSettled 确保不会因单张失败中断整层
|
||||
const results = await Promise.allSettled(levelItems.map((item) => generateOneImage(item)));
|
||||
|
||||
// 处理失败的任务
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
if (results[i].status === "rejected") {
|
||||
const failedId = levelIds[i];
|
||||
const reason = (results[i] as PromiseRejectedResult).reason;
|
||||
console.error(`[tools] 分镜 id:${failedId} 图片生成失败`, reason);
|
||||
thinking.appendText(`分镜 id:${failedId} 图片生成失败: ${reason?.message || reason}`);
|
||||
await u.db("o_storyboard").where("id", failedId).update({ state: "生成失败" });
|
||||
socket.emit("setFlowData", {
|
||||
key: "setStoryboardImage",
|
||||
value: { id: failedId, src: "", state: "生成失败" },
|
||||
});
|
||||
}
|
||||
}
|
||||
thinking.appendText(`第 ${levelIndex + 1}/${levels.length} 层图片生成完成`);
|
||||
thinking.complete();
|
||||
}
|
||||
thinking.appendText("所有分镜图片生成完成");
|
||||
thinking.updateTitle("分镜图片生成完成");
|
||||
thinking.complete();
|
||||
|
||||
return "分镜图片生成完成";
|
||||
},
|
||||
}),
|
||||
|
||||
//todo 提示词待调
|
||||
generate_assets_images: tool({
|
||||
description: `
|
||||
生成 资产图片 不区分原资产于衍生资产
|
||||
参数说明:
|
||||
- images: 图片任务数组
|
||||
- assetId: 资产id
|
||||
- prompt: 图片生成提示词
|
||||
示例:
|
||||
images:[
|
||||
{assetId: 1, prompt: "一张猫的图片"}
|
||||
]
|
||||
`,
|
||||
inputSchema: z.object({
|
||||
images: z.array(
|
||||
z.object({
|
||||
assetId: z.number().describe("衍生资产id"),
|
||||
prompt: z.string().describe("提示词"),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
execute: async ({ images }) => {
|
||||
console.log("[tools] generate_assets_images", images);
|
||||
//先获取到前端资产数据
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "assets" }, (res: any) => resolve(res)));
|
||||
const assetsData = flowData["assets"];
|
||||
const assetsImage: { assetId: number; prompt: string; id?: number }[] = [...images];
|
||||
//获取对应的 原资产id
|
||||
assetsImage.forEach((item) => {
|
||||
for (const i of assetsData) {
|
||||
const findData = i.derive.find((m) => m.id == item.assetId);
|
||||
if (findData) {
|
||||
item.id = findData.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
//获取所设置模型
|
||||
const imageModelData = await u.db("o_project").where("id", resTool.data.projectId).select("imageModel", "imageQuality").first();
|
||||
for (const item of assetsImage) {
|
||||
const [imageId] = await u.db("o_image").insert({
|
||||
// 数据库插入图片记录
|
||||
assetsId: item.assetId,
|
||||
model: imageModelData?.imageModel,
|
||||
state: "生成中",
|
||||
resolution: imageModelData?.imageQuality,
|
||||
});
|
||||
u.Ai.Image(imageModelData?.imageModel)
|
||||
.run({
|
||||
prompt: item.prompt,
|
||||
imageBase64: await getAssetsImageBase64(item.id ? [item.id] : []),
|
||||
size: imageModelData?.imageQuality,
|
||||
aspectRatio: "16:9",
|
||||
taskClass: "生成图片",
|
||||
describe: "资产图片生成",
|
||||
relatedObjects: "hhhh",
|
||||
projectId: resTool.data.projectId,
|
||||
})
|
||||
.then(async (imageCls) => {
|
||||
const savePath = `/${resTool.data.projectId}/assets/${u.uuid()}.jpg`;
|
||||
await imageCls.save(savePath);
|
||||
const obj = {
|
||||
...item,
|
||||
id: item.assetId,
|
||||
src: await u.oss.getFileUrl(savePath),
|
||||
state: "已完成",
|
||||
};
|
||||
//更新对应数据库
|
||||
await u.db("o_assets").where("id", item.assetId).update({ imageId: imageId });
|
||||
await u.db("o_image").where({ id: imageId }).update({ state: "已完成", filePath: savePath });
|
||||
//通知前端更新
|
||||
socket.emit("setFlowData", { key: "setAssetsImage", value: obj });
|
||||
});
|
||||
//通知前端更新状态
|
||||
socket.emit("setFlowData", { key: "setAssetsImage", value: { ...item, id: item.assetId, src: "", state: "生成中" } });
|
||||
}
|
||||
return "资产生成中";
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
return toolsNames ? Object.fromEntries(Object.entries(tools).filter(([n]) => toolsNames.includes(n))) : tools;
|
||||
};
|
||||
|
||||
// 获取资产图片base64
|
||||
async function getAssetsImageBase64(imageIds: number[]) {
|
||||
if (imageIds.length === 0) return [];
|
||||
const imagePaths = await u
|
||||
.db("o_assets")
|
||||
.leftJoin("o_image", "o_assets.imageId", "o_image.id")
|
||||
.whereIn("o_assets.id", imageIds)
|
||||
.select("o_assets.id", "o_image.filePath");
|
||||
if (!imagePaths.length) return [];
|
||||
const imageUrls = await Promise.all(
|
||||
imagePaths.map(async (i) => {
|
||||
if (i.filePath) {
|
||||
try {
|
||||
return await urlToBase64(await u.oss.getFileUrl(i.filePath));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
return imageUrls.filter(Boolean) as string[];
|
||||
}
|
||||
|
||||
//获取分镜图片base64
|
||||
async function getStoryboardImageBase64(imageIds: number[]) {
|
||||
if (!imageIds.length) return [];
|
||||
const storayboardData = await u.db("o_storyboard").whereIn("id", imageIds).select("id", "filePath");
|
||||
const imageUrls = await Promise.all(
|
||||
storayboardData.map(async (i) => {
|
||||
if (i.filePath) {
|
||||
try {
|
||||
return await urlToBase64(await u.oss.getFileUrl(i.filePath));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
return imageUrls.filter(Boolean) as string[];
|
||||
}
|
||||
@ -3,8 +3,8 @@ import { z } from "zod";
|
||||
import _ from "lodash";
|
||||
import ResTool from "@/socket/resTool";
|
||||
import u from "@/utils";
|
||||
import { urlToBase64 } from "@/utils/vm";
|
||||
export const deriveAssetSchema = z.object({
|
||||
|
||||
const deriveAssetSchema = z.object({
|
||||
id: z.number().describe("衍生资产ID,如果新增则为空"),
|
||||
assetsId: z.number().describe("关联的资产ID"),
|
||||
prompt: z.string().describe("生成提示词"),
|
||||
@ -14,7 +14,7 @@ export const deriveAssetSchema = z.object({
|
||||
state: z.enum(["未生成", "生成中", "已完成", "生成失败"]).describe("衍生资产生成状态"),
|
||||
type: z.enum(["role", "tool", "scene", "clip"]).describe("衍生资产类型"),
|
||||
});
|
||||
export const assetItemSchema = z.object({
|
||||
const assetItemSchema = z.object({
|
||||
id: z.number().describe("资产唯一标识"),
|
||||
name: z.string().describe("资产名称"),
|
||||
type: z.enum(["role", "tool", "scene", "clip"]).describe("资产类型"),
|
||||
@ -22,7 +22,7 @@ export const assetItemSchema = z.object({
|
||||
desc: z.string().describe("资产描述"),
|
||||
derive: z.array(deriveAssetSchema).describe("衍生资产列表"),
|
||||
});
|
||||
export const storyboardSchema = z.object({
|
||||
const storyboardSchema = z.object({
|
||||
id: z.number().describe("分镜ID,必须为真实id"),
|
||||
title: z.string().describe("分镜标题"),
|
||||
description: z.string().describe("分镜描述"),
|
||||
@ -41,7 +41,7 @@ export const storyboardSchema = z.object({
|
||||
associateAssetsIds: z.array(z.number()).describe("关联资产ID列表"),
|
||||
src: z.string().nullable().describe("分镜资源路径"),
|
||||
});
|
||||
export const workbenchDataSchema = z.object({
|
||||
const workbenchDataSchema = z.object({
|
||||
name: z.string().describe("项目名称"),
|
||||
duration: z.string().describe("视频时长"),
|
||||
resolution: z.string().describe("分辨率"),
|
||||
@ -49,11 +49,11 @@ export const workbenchDataSchema = z.object({
|
||||
cover: z.string().optional().describe("封面图片路径"),
|
||||
gradient: z.string().optional().describe("渐变色配置"),
|
||||
});
|
||||
export const posterItemSchema = z.object({
|
||||
const posterItemSchema = z.object({
|
||||
id: z.number().describe("海报ID"),
|
||||
image: z.string().describe("海报图片路径"),
|
||||
});
|
||||
export const flowDataSchema = z.object({
|
||||
const flowDataSchema = z.object({
|
||||
script: z.string().describe("剧本内容"),
|
||||
scriptPlan: z.string().describe("拍摄计划"),
|
||||
assets: z.array(assetItemSchema).describe("衍生资产"),
|
||||
@ -93,659 +93,94 @@ export default (toolCpnfig: ToolConfig) => {
|
||||
const thinking = msg.thinking(`正在获取${flowDataKeyLabels[key]}工作区数据...`);
|
||||
console.log("[tools] get_flowData", key);
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key }, (res: any) => resolve(res)));
|
||||
thinking.appendText(`获取到${flowDataKeyLabels[key]}:\n` + flowData[key]);
|
||||
thinking.appendText(`获取到${flowDataKeyLabels[key]}:\n` + JSON.stringify(flowData[key], null, 2));
|
||||
thinking.updateTitle(`获取${flowDataKeyLabels[key]}完成`);
|
||||
thinking.complete();
|
||||
return flowData[key];
|
||||
},
|
||||
}),
|
||||
set_flowData_script: tool({
|
||||
description: "保存剧本内容到工作区",
|
||||
inputSchema: z.object({ value: flowDataSchema.shape.script }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_flowData script", value);
|
||||
const thinking = msg.thinking("正在保存 剧本 数据");
|
||||
socket.emit("setFlowData", { key: "script", value });
|
||||
thinking.updateTitle("保存 剧本 数据完成");
|
||||
thinking.complete();
|
||||
return true;
|
||||
},
|
||||
add_deriveAsset: tool({
|
||||
description: "新增或更新衍生资产",
|
||||
inputSchema: z.object({
|
||||
assetsId: z.number().describe("关联的资产ID"),
|
||||
id: z.number().nullable().describe("衍生资产ID,如果新增则为空"),
|
||||
name: z.string().describe("衍生资产名称"),
|
||||
desc: z.string().describe("衍生资产描述"),
|
||||
type: z.enum(["role", "tool", "scene", "clip"]).describe("衍生资产类型"),
|
||||
}),
|
||||
set_flowData_scriptPlan: tool({
|
||||
description: "保存拍摄计划到工作区",
|
||||
inputSchema: z.object({ value: flowDataSchema.shape.scriptPlan }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_flowData scriptPlan", value);
|
||||
const thinking = msg.thinking("正在保存 拍摄计划 数据");
|
||||
socket.emit("setFlowData", { key: "scriptPlan", value });
|
||||
thinking.updateTitle("保存 拍摄计划 数据完成");
|
||||
thinking.complete();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
add_flowData_assets: tool({
|
||||
description: "新增对应衍生资产列表到工作区,严禁包含 不需要新增的数据",
|
||||
inputSchema: z.object({ value: z.array(deriveAssetSchema.omit({ id: true })).describe("需要新增的衍生资产列表") }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_flowData add_flowData_assets", value);
|
||||
const thinking = msg.thinking("正在保存 衍生资产 数据");
|
||||
const setData = [...value] as z.infer<typeof deriveAssetSchema>[];
|
||||
execute: async (deriveAsset) => {
|
||||
const thinking = msg.thinking("正在操作资产...");
|
||||
const { projectId, scriptId } = resTool.data;
|
||||
const startTime = Date.now();
|
||||
|
||||
// 并行插入所有 o_assets 记录
|
||||
await Promise.all(
|
||||
setData.map(async (i) => {
|
||||
const [insertedId] = await u.db("o_assets").insert({
|
||||
assetsId: +i.assetsId || null,
|
||||
const data = {
|
||||
id: deriveAsset.id ?? undefined,
|
||||
assetsId: deriveAsset.assetsId,
|
||||
projectId,
|
||||
name: i.name,
|
||||
type: i.type,
|
||||
prompt: i.prompt,
|
||||
describe: i.desc,
|
||||
name: deriveAsset.name,
|
||||
type: deriveAsset.type,
|
||||
describe: deriveAsset.desc,
|
||||
startTime,
|
||||
});
|
||||
i.id = insertedId;
|
||||
}),
|
||||
);
|
||||
|
||||
// 批量插入 o_scriptAssets
|
||||
await u.db("o_scriptAssets").insert(setData.map((i) => ({ scriptId, assetId: i.id })));
|
||||
|
||||
const watiAddAssetsMap: Record<number, z.infer<typeof deriveAssetSchema>[]> = {};
|
||||
setData.forEach((i) => {
|
||||
if (watiAddAssetsMap[i.assetsId]) {
|
||||
watiAddAssetsMap[i.assetsId].push(i);
|
||||
};
|
||||
if (deriveAsset.id) {
|
||||
await u.db("o_assets").where("id", deriveAsset.id).update(data);
|
||||
thinking.appendText(`已更新衍生资产,ID: ${deriveAsset.id}\n`);
|
||||
} else {
|
||||
watiAddAssetsMap[i.assetsId] = [i];
|
||||
const [insertedId] = await u.db("o_assets").insert(data);
|
||||
data.id = insertedId;
|
||||
await u.db("o_scriptAssets").insert({ scriptId, assetId: insertedId });
|
||||
thinking.appendText(`已新增衍生资产,ID: ${insertedId}\n`);
|
||||
}
|
||||
});
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "assets" }, (res: any) => resolve(res)));
|
||||
const assetsData = flowData.assets;
|
||||
assetsData.forEach((i) => {
|
||||
if (watiAddAssetsMap[i.id]) {
|
||||
i.derive = [...(i.derive || []), ...watiAddAssetsMap[i.id]];
|
||||
}
|
||||
});
|
||||
thinking.updateTitle("保存 衍生资产 数据完成");
|
||||
const res = await new Promise((resolve) => socket.emit("addDeriveAsset", data, (res: any) => resolve(res)));
|
||||
thinking.updateTitle("资产操作完成");
|
||||
thinking.complete();
|
||||
|
||||
socket.emit("setFlowData", { key: "assets", value: assetsData });
|
||||
return true;
|
||||
return res ?? "操作成功";
|
||||
},
|
||||
}),
|
||||
update_flowData_assets: tool({
|
||||
description: "更新对应衍生资产列表到工作区",
|
||||
inputSchema: z.object({ value: z.array(deriveAssetSchema).describe("需要更新的衍生资产列表") }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] update_flowData update_flowData_assets", value);
|
||||
const thinking = msg.thinking("正在保存 衍生资产 数据");
|
||||
for (const i of value) {
|
||||
await u
|
||||
.db("o_assets")
|
||||
.where("id", i.id)
|
||||
.update({
|
||||
assetsId: +i.assetsId || null,
|
||||
projectId: resTool.data.projectId,
|
||||
name: i.name,
|
||||
type: i.type,
|
||||
prompt: i.prompt,
|
||||
describe: i.desc,
|
||||
});
|
||||
}
|
||||
// 按 assetsId 分组,构建更新映射
|
||||
const updateAssetsMap: Record<number, z.infer<typeof deriveAssetSchema>[]> = {};
|
||||
value.forEach((i) => {
|
||||
if (updateAssetsMap[i.assetsId]) {
|
||||
updateAssetsMap[i.assetsId].push(i);
|
||||
} else {
|
||||
updateAssetsMap[i.assetsId] = [i];
|
||||
}
|
||||
});
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "assets" }, (res: any) => resolve(res)));
|
||||
const assetsData = flowData.assets;
|
||||
// 将 derive 中已存在的条目替换为更新后的数据
|
||||
assetsData.forEach((asset) => {
|
||||
if (updateAssetsMap[asset.id]) {
|
||||
const updatedMap = Object.fromEntries(updateAssetsMap[asset.id].map((d) => [d.id, d]));
|
||||
asset.derive = (asset.derive || []).map((d) => updatedMap[d.id] ?? d);
|
||||
}
|
||||
});
|
||||
thinking.updateTitle("保存 衍生资产 数据完成");
|
||||
thinking.complete();
|
||||
socket.emit("setFlowData", { key: "assets", value: assetsData });
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
delete_flowData_assets: tool({
|
||||
description: "删除对应衍生资产",
|
||||
inputSchema: z.object({ ids: z.array(z.number()).describe("需要删除的 衍生资产id ") }),
|
||||
execute: async ({ ids }) => {
|
||||
console.log("[tools] delete_flowData delete_flowData_assets", ids);
|
||||
const thinking = msg.thinking("正在删除指定 衍生资产 数据...");
|
||||
await u.db("o_assets").whereIn("id", ids).delete();
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "assets" }, (res: any) => resolve(res)));
|
||||
const assetsData = flowData.assets;
|
||||
assetsData.forEach((i) => {
|
||||
i.derive = (i.derive || []).filter((d) => !ids.includes(d.id));
|
||||
});
|
||||
thinking.updateTitle("删除指定 衍生资产 数据完成");
|
||||
thinking.complete();
|
||||
// 将 derive 中已存在的条目替换为更新后的数据
|
||||
socket.emit("setFlowData", { key: "assets", value: assetsData });
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
// set_flowData_assets: tool({
|
||||
// description: "保存衍生资产列表到工作区",
|
||||
// inputSchema: z.object({ value: flowDataSchema.shape.assets }),
|
||||
// execute: async ({ value }) => {
|
||||
// console.log("[tools] set_flowData assets", value);
|
||||
// resTool.systemMessage("正在保存 衍生资产 数据");
|
||||
// if (value && Array.isArray(value) && value.length) {
|
||||
// for (const i of value) {
|
||||
// if (!i?.id) {
|
||||
// const [insertedId] = await u.db("o_assets").insert({
|
||||
// assetsId: null,
|
||||
// name: i.name,
|
||||
// type: i.type,
|
||||
// prompt: i.prompt,
|
||||
// describe: i.desc,
|
||||
// startTime: Date.now(),
|
||||
// });
|
||||
// i.id = insertedId;
|
||||
// }
|
||||
// if (i.derive && Array.isArray(i.derive) && i.derive.length) {
|
||||
// for (const sub of i.derive) {
|
||||
// if (sub.id) continue;
|
||||
// const [insertedId] = await u.db("o_assets").insert({
|
||||
// assetsId: +i.id || null,
|
||||
// projectId: resTool.data.projectId,
|
||||
// name: sub.name,
|
||||
// type: sub.type,
|
||||
// prompt: sub.prompt,
|
||||
// describe: sub.desc,
|
||||
// startTime: Date.now(),
|
||||
// });
|
||||
// await u.db("o_scriptAssets").insert({
|
||||
// scriptId: resTool.data.scriptId,
|
||||
// assetId: insertedId,
|
||||
// });
|
||||
// sub.id = insertedId;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// socket.emit("setFlowData", { key: "assets", value });
|
||||
// return true;
|
||||
// },
|
||||
// }),
|
||||
set_flowData_storyboardTable: tool({
|
||||
description: "保存分镜表到工作区",
|
||||
inputSchema: z.object({ value: flowDataSchema.shape.storyboardTable }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_flowData storyboardTable", value);
|
||||
const thinking = msg.thinking("正在保存 分镜表 数据...");
|
||||
socket.emit("setFlowData", { key: "storyboardTable", value });
|
||||
thinking.updateTitle("保存 分镜表 数据完成");
|
||||
thinking.complete();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
add_flowData_storyboard: tool({
|
||||
description: "新增分镜面板到工作区",
|
||||
inputSchema: z.object({ value: z.array(storyboardSchema.omit({ id: true })) }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] add_flowData storyboard", value);
|
||||
const thinking = msg.thinking("正在保存 分镜面板 数据...");
|
||||
const setData = [...value] as z.infer<typeof storyboardSchema>[];
|
||||
for (const item of setData) {
|
||||
item.src = "";
|
||||
const [insertedId] = await u.db("o_storyboard").insert({
|
||||
title: item.title,
|
||||
prompt: item.prompt,
|
||||
description: item.description,
|
||||
frameMode: item.frameMode,
|
||||
duration: String(item.duration),
|
||||
camera: item.camera,
|
||||
sound: item.sound,
|
||||
lines: item.lines,
|
||||
state: "未生成",
|
||||
scriptId: resTool.data.scriptId,
|
||||
createTime: Date.now(),
|
||||
});
|
||||
if (item.associateAssetsIds.length) {
|
||||
await u.db("o_assets2Storyboard").insert(item.associateAssetsIds.map((i) => ({ storyboardId: insertedId, assetId: i })));
|
||||
}
|
||||
item.id = insertedId;
|
||||
}
|
||||
//为了防止丢失分镜其他数据,例如:依赖分镜Id、依赖资产idc
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "storyboard" }, (res: any) => resolve(res)));
|
||||
const storyboardData = flowData["storyboard"].concat([...setData]);
|
||||
socket.emit("setFlowData", { key: "storyboard", value: storyboardData });
|
||||
|
||||
thinking.updateTitle("保存 分镜面板 数据完成");
|
||||
thinking.complete();
|
||||
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
update_flowData_storyboard: tool({
|
||||
description: "更新指定分镜面板到工作区",
|
||||
inputSchema: z.object({ value: flowDataSchema.shape.storyboard }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] update_flowData storyboard", value);
|
||||
const thinking = msg.thinking("正在保存 分镜面板 数据...");
|
||||
for (const item of value) {
|
||||
await u
|
||||
.db("o_storyboard")
|
||||
.where("id", item.id)
|
||||
.update({
|
||||
title: item.title,
|
||||
prompt: item.prompt,
|
||||
description: item.description,
|
||||
frameMode: item.frameMode,
|
||||
duration: String(item.duration),
|
||||
camera: item.camera,
|
||||
sound: item.sound,
|
||||
lines: item.lines,
|
||||
});
|
||||
}
|
||||
//直接拉取前端数据,为了防止丢失分镜其他数据,例如:依赖分镜Id、依赖资产idc
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "storyboard" }, (res: any) => resolve(res)));
|
||||
const storyboardData = flowData["storyboard"].map((existing) => {
|
||||
const updated = value.find((v) => v.id === existing.id);
|
||||
if (!updated) return existing;
|
||||
return {
|
||||
...existing,
|
||||
title: updated.title,
|
||||
prompt: updated.prompt,
|
||||
description: updated.description,
|
||||
frameMode: updated.frameMode,
|
||||
duration: updated.duration,
|
||||
camera: updated.camera,
|
||||
sound: updated.sound,
|
||||
lines: updated.lines,
|
||||
};
|
||||
});
|
||||
socket.emit("setFlowData", { key: "storyboard", value: storyboardData });
|
||||
thinking.updateTitle("保存 分镜面板 数据完成");
|
||||
thinking.complete();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
delete_flowData_storyboard: tool({
|
||||
description: "删除指定分镜面板并更新工作区",
|
||||
inputSchema: z.object({ ids: z.array(z.number()).describe("需要删除的 分镜id ") }),
|
||||
execute: async ({ ids }) => {
|
||||
console.log("[tools] delete_flowData storyboard", ids);
|
||||
const thinking = msg.thinking("正在删除指定 分镜面板 数据...");
|
||||
await u.db("o_storyboard").whereIn("id", ids).delete();
|
||||
await u.db("o_assets2Storyboard").whereIn("storyboardId", ids).delete();
|
||||
await u.db("o_storyboardFlow").whereIn("storyboardId", ids).delete();
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "storyboard" }, (res: any) => resolve(res)));
|
||||
const storyboardData = flowData["storyboard"].filter((item) => !ids.includes(item.id));
|
||||
socket.emit("setFlowData", { key: "storyboard", value: storyboardData });
|
||||
thinking.updateTitle("删除指定 分镜面板 数据完成");
|
||||
thinking.complete();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
// set_flowData_storyboard: tool({
|
||||
// description: "保存分镜面板到工作区",
|
||||
// inputSchema: z.object({ value: flowDataSchema.shape.storyboard }),
|
||||
// execute: async ({ value }) => {
|
||||
// console.log("[tools] set_flowData storyboard", value);
|
||||
// resTool.systemMessage("正在保存 分镜面板 数据...");
|
||||
// for (const item of value) {
|
||||
// if (!item.id) {
|
||||
// const [insertedId] = await u.db("o_storyboard").insert({
|
||||
// title: item.title,
|
||||
// prompt: item.prompt,
|
||||
// description: item.description,
|
||||
// filePath: item.src,
|
||||
// frameMode: item.frameMode,
|
||||
// duration: String(item.duration),
|
||||
// camera: item.camera,
|
||||
// sound: item.sound,
|
||||
// lines: item.lines,
|
||||
// state: "未生成",
|
||||
// scriptId: resTool.data.scriptId,
|
||||
// });
|
||||
// console.log("%c Line:216 🥥 item.associateAssetsIds", "background:#6ec1c2", item.associateAssetsIds);
|
||||
|
||||
// if (item.associateAssetsIds.length) {
|
||||
// await u.db("o_assets2Storyboard").insert(item.associateAssetsIds.map((i) => ({ storyboardId: insertedId, assetId: i })));
|
||||
// }
|
||||
// item.id = insertedId;
|
||||
// }
|
||||
// }
|
||||
// socket.emit("setFlowData", { key: "storyboard", value });
|
||||
// return true;
|
||||
// },
|
||||
// }),
|
||||
set_flowData_workbench: tool({
|
||||
description: "保存工作台配置数据到工作区",
|
||||
inputSchema: z.object({ value: flowDataSchema.shape.workbench }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_flowData workbench", value);
|
||||
const thinking = msg.thinking("正在保存 工作台配置 数据...");
|
||||
socket.emit("setFlowData", { key: "workbench", value });
|
||||
thinking.updateTitle("保存 工作台配置 数据完成");
|
||||
thinking.complete();
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
set_flowData_poster: tool({
|
||||
description: "保存海报配置到工作区",
|
||||
inputSchema: z.object({ value: flowDataSchema.shape.poster }),
|
||||
execute: async ({ value }) => {
|
||||
console.log("[tools] set_flowData poster", value);
|
||||
const thinking = msg.thinking("正在保存 海报配置 数据...");
|
||||
thinking.updateTitle("保存 海报配置 数据完成");
|
||||
thinking.complete();
|
||||
socket.emit("setFlowData", { key: "poster", value });
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
// todo 提示词待调
|
||||
generate_storyboard_images: tool({
|
||||
description: `生成一组图片任务,支持图片间的依赖关系(以图生图),基于有向无环图(DAG)拓扑排序执行。
|
||||
|
||||
参数说明:
|
||||
- images: 图片任务数组
|
||||
- id: 图片唯一标识符(分镜id)
|
||||
- prompt: 图片生成提示词
|
||||
- referenceIds: 依赖的参考图id数组,无依赖填空数组[]
|
||||
- assetIds: 参考的资产图id数组(可选)
|
||||
|
||||
依赖规则:
|
||||
1. referenceIds中的id必须存在于images数组中
|
||||
2. 禁止循环依赖(如A依赖B,B依赖A)
|
||||
3. 被依赖的图片会先生成,其结果作为参考图传入
|
||||
|
||||
示例:生成猫图,再以猫图为参考生成狗图
|
||||
images: [
|
||||
{id: 1, prompt: "一只橘猫", referenceIds: [], assetIds: []},
|
||||
{id: 2, prompt: "风格相同的金毛犬", referenceIds: [1], assetIds: []}
|
||||
]`,
|
||||
del_deriveAsset: tool({
|
||||
description: "删除衍生资产",
|
||||
inputSchema: z.object({
|
||||
images: z.array(
|
||||
z.object({
|
||||
id: z.number().describe("从工作区获取到的分镜id"),
|
||||
prompt: z.string().describe("图片生成提示词"),
|
||||
referenceIds: z.array(z.number()).describe("依赖的参考 分镜图id数组,无依赖填空数组[]"),
|
||||
assetIds: z.array(z.number()).describe("参考的资产图"),
|
||||
assetsId: z.number().describe("关联的资产ID"),
|
||||
id: z.number().describe("衍生资产ID"),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
execute: async ({ images }) => {
|
||||
console.log("[tools] generate_storyboard_images", images);
|
||||
const thinking = msg.thinking("正在生成 分镜图片 数据...");
|
||||
// --- 构建任务id集合 ---
|
||||
const taskIds = new Set(images.map((item) => item.id));
|
||||
const imageMap = new Map(images.map((item) => [item.id, item]));
|
||||
|
||||
// --- 检测循环依赖 (Kahn算法拓扑排序) ---
|
||||
// 将 referenceIds 分为:本批次内依赖 vs 外部已有依赖
|
||||
// 只有本批次内的依赖才参与 DAG 调度,外部依赖直接从数据库获取
|
||||
const inDegree = new Map<number, number>();
|
||||
// adjacency: 被依赖者 -> 依赖它的节点列表
|
||||
const adjacency = new Map<number, number[]>();
|
||||
|
||||
for (const item of images) {
|
||||
// 只统计本批次内的依赖作为入度
|
||||
const internalDeps = item.referenceIds.filter((refId) => taskIds.has(refId));
|
||||
inDegree.set(item.id, internalDeps.length);
|
||||
for (const depId of internalDeps) {
|
||||
if (!adjacency.has(depId)) adjacency.set(depId, []);
|
||||
adjacency.get(depId)!.push(item.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 拓扑排序,按层级分组(同层可并行)
|
||||
const levels: number[][] = [];
|
||||
let queue = images.filter((item) => (inDegree.get(item.id) ?? 0) === 0).map((item) => item.id);
|
||||
|
||||
const visited = new Set<number>();
|
||||
while (queue.length > 0) {
|
||||
levels.push([...queue]);
|
||||
const nextQueue: number[] = [];
|
||||
for (const nodeId of queue) {
|
||||
visited.add(nodeId);
|
||||
for (const childId of adjacency.get(nodeId) ?? []) {
|
||||
inDegree.set(childId, (inDegree.get(childId) ?? 1) - 1);
|
||||
if (inDegree.get(childId) === 0) {
|
||||
nextQueue.push(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
queue = nextQueue;
|
||||
}
|
||||
// 循环依赖检测
|
||||
if (visited.size !== images.length) {
|
||||
const cyclicIds = images.filter((item) => !visited.has(item.id)).map((item) => item.id);
|
||||
thinking.appendText(`检测到循环依赖,涉及分镜id: ${cyclicIds.join(", ")},请修正后重试`);
|
||||
thinking.updateTitle("循环依赖错误");
|
||||
thinking.error();
|
||||
return `错误:检测到循环依赖,涉及分镜id: ${cyclicIds.join(", ")}`;
|
||||
}
|
||||
|
||||
thinking.appendText(`图片生成调度计划:共 ${levels.length} 层,${images.length} 张图片`);
|
||||
|
||||
// --- 准备公共数据 ---
|
||||
const projectData = await u.db("o_project").where("id", resTool.data.projectId).select("videoRatio").first();
|
||||
const imageModelData = await u.db("o_project").where("id", resTool.data.projectId).select("imageModel", "imageQuality").first();
|
||||
|
||||
// 生成单张图片的函数
|
||||
const generateOneImage = async (item: (typeof images)[0]) => {
|
||||
const thinking = msg.thinking(`正在生成分镜 id:${item.id} 图片`);
|
||||
// 更新数据库状态为生成中
|
||||
await u.db("o_storyboard").where("id", item.id).update({ state: "生成中" });
|
||||
// 更新前端为生成中
|
||||
socket.emit("setFlowData", {
|
||||
key: "setStoryboardImage",
|
||||
value: { ...item, id: item.id, src: "", state: "生成中", referenceIds: item.referenceIds },
|
||||
});
|
||||
|
||||
// 获取参考图base64(包括资产图和已生成的分镜参考图)
|
||||
const [assetsBase64, referenceBase64] = await Promise.all([
|
||||
getAssetsImageBase64(item.assetIds ?? []),
|
||||
getStoryboardImageBase64(item.referenceIds),
|
||||
]);
|
||||
|
||||
const imageCls = await u.Ai.Image(imageModelData.imageModel).run({
|
||||
prompt: item.prompt,
|
||||
imageBase64: [...assetsBase64, ...referenceBase64],
|
||||
size: imageModelData.imageQuality,
|
||||
aspectRatio: (projectData?.videoRatio as `${number}:${number}`) ?? "16:9",
|
||||
taskClass: "生成图片",
|
||||
describe: "分镜图片生成",
|
||||
relatedObjects: "hhhh",
|
||||
projectId: resTool.data.projectId,
|
||||
});
|
||||
|
||||
const savePath = `/${resTool.data.projectId}/storyboard/${u.uuid()}.jpg`;
|
||||
await imageCls.save(savePath);
|
||||
|
||||
// 更新数据库状态为已完成
|
||||
await u.db("o_storyboard").where("id", item.id).update({ state: "已完成", filePath: savePath });
|
||||
|
||||
const obj = {
|
||||
...item,
|
||||
id: item.id,
|
||||
src: await u.oss.getFileUrl(savePath),
|
||||
state: "已完成",
|
||||
referenceIds: item.referenceIds,
|
||||
};
|
||||
// 前端对话框提示
|
||||
thinking.appendText(`分镜 id:${item.id} 图片生成完成`);
|
||||
execute: async ({ assetsId, id }) => {
|
||||
const thinking = msg.thinking("正在操作资产...");
|
||||
const { scriptId } = resTool.data;
|
||||
await u.db("o_assets").where("id", id).del();
|
||||
await u.db("o_scriptAssets").where({ scriptId, assetId: id }).del();
|
||||
thinking.appendText(`已删除衍生资产,ID: ${id}\n`);
|
||||
const res = await new Promise((resolve) => socket.emit("delDeriveAsset", { assetsId, id }, (res: any) => resolve(res)));
|
||||
thinking.updateTitle("资产操作完成");
|
||||
thinking.complete();
|
||||
// 更新前端界面展示
|
||||
socket.emit("setFlowData", { key: "setStoryboardImage", value: obj });
|
||||
};
|
||||
|
||||
// --- 按层级顺序执行:同层并行,层间串行 ---
|
||||
for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
|
||||
const levelIds = levels[levelIndex];
|
||||
const levelItems = levelIds.map((id) => imageMap.get(id)!);
|
||||
const thinking = msg.thinking(
|
||||
`开始生成第 ${levelIndex + 1}/${levels.length} 层,共 ${levelItems.length} 张图片 (ids: ${levelIds.join(", ")})`,
|
||||
);
|
||||
|
||||
// 同层内所有图片并行生成,使用 allSettled 确保不会因单张失败中断整层
|
||||
const results = await Promise.allSettled(levelItems.map((item) => generateOneImage(item)));
|
||||
|
||||
// 处理失败的任务
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
if (results[i].status === "rejected") {
|
||||
const failedId = levelIds[i];
|
||||
const reason = (results[i] as PromiseRejectedResult).reason;
|
||||
console.error(`[tools] 分镜 id:${failedId} 图片生成失败`, reason);
|
||||
thinking.appendText(`分镜 id:${failedId} 图片生成失败: ${reason?.message || reason}`);
|
||||
await u.db("o_storyboard").where("id", failedId).update({ state: "生成失败" });
|
||||
socket.emit("setFlowData", {
|
||||
key: "setStoryboardImage",
|
||||
value: { id: failedId, src: "", state: "生成失败" },
|
||||
});
|
||||
}
|
||||
}
|
||||
thinking.appendText(`第 ${levelIndex + 1}/${levels.length} 层图片生成完成`);
|
||||
thinking.complete();
|
||||
}
|
||||
thinking.appendText("所有分镜图片生成完成");
|
||||
thinking.updateTitle("分镜图片生成完成");
|
||||
thinking.complete();
|
||||
|
||||
return "分镜图片生成完成";
|
||||
return res ?? "删除成功";
|
||||
},
|
||||
}),
|
||||
|
||||
//todo 提示词待调
|
||||
generate_assets_images: tool({
|
||||
description: `
|
||||
生成 资产图片 不区分原资产于衍生资产
|
||||
参数说明:
|
||||
- images: 图片任务数组
|
||||
- assetId: 资产id
|
||||
- prompt: 图片生成提示词
|
||||
示例:
|
||||
images:[
|
||||
{assetId: 1, prompt: "一张猫的图片"}
|
||||
]
|
||||
`,
|
||||
generate_deriveAsset: tool({
|
||||
description: "生成衍生资产",
|
||||
inputSchema: z.object({
|
||||
images: z.array(
|
||||
z.object({
|
||||
assetId: z.number().describe("衍生资产id"),
|
||||
prompt: z.string().describe("提示词"),
|
||||
id: z.number().describe("衍生资产ID"),
|
||||
}),
|
||||
),
|
||||
execute: async ({ id }) => {
|
||||
const thinking = msg.thinking("正在生成衍生资产...");
|
||||
const res = await new Promise((resolve) => socket.emit("generateDeriveAsset", { id }, (res: any) => resolve(res)));
|
||||
thinking.appendText(`已生成衍生资产,ID: ${id}\n`);
|
||||
thinking.updateTitle("衍生资产生成完成");
|
||||
thinking.complete();
|
||||
return res ?? "生成失败";
|
||||
},
|
||||
}),
|
||||
execute: async ({ images }) => {
|
||||
console.log("[tools] generate_assets_images", images);
|
||||
//先获取到前端资产数据
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "assets" }, (res: any) => resolve(res)));
|
||||
const assetsData = flowData["assets"];
|
||||
const assetsImage: { assetId: number; prompt: string; id?: number }[] = [...images];
|
||||
//获取对应的 原资产id
|
||||
assetsImage.forEach((item) => {
|
||||
for (const i of assetsData) {
|
||||
const findData = i.derive.find((m) => m.id == item.assetId);
|
||||
if (findData) {
|
||||
item.id = findData.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
//获取所设置模型
|
||||
const imageModelData = await u.db("o_project").where("id", resTool.data.projectId).select("imageModel", "imageQuality").first();
|
||||
for (const item of assetsImage) {
|
||||
const [imageId] = await u.db("o_image").insert({
|
||||
// 数据库插入图片记录
|
||||
assetsId: item.assetId,
|
||||
model: imageModelData?.imageModel,
|
||||
state: "生成中",
|
||||
resolution: imageModelData?.imageQuality,
|
||||
});
|
||||
u.Ai.Image(imageModelData?.imageModel)
|
||||
.run({
|
||||
prompt: item.prompt,
|
||||
imageBase64: await getAssetsImageBase64(item.id ? [item.id] : []),
|
||||
size: imageModelData?.imageQuality,
|
||||
aspectRatio: "16:9",
|
||||
taskClass: "生成图片",
|
||||
describe: "资产图片生成",
|
||||
relatedObjects: "hhhh",
|
||||
projectId: resTool.data.projectId,
|
||||
})
|
||||
.then(async (imageCls) => {
|
||||
const savePath = `/${resTool.data.projectId}/assets/${u.uuid()}.jpg`;
|
||||
await imageCls.save(savePath);
|
||||
const obj = {
|
||||
...item,
|
||||
id: item.assetId,
|
||||
src: await u.oss.getFileUrl(savePath),
|
||||
state: "已完成",
|
||||
};
|
||||
//更新对应数据库
|
||||
await u.db("o_assets").where("id", item.assetId).update({ imageId: imageId });
|
||||
await u.db("o_image").where({ id: imageId }).update({ state: "已完成", filePath: savePath });
|
||||
//通知前端更新
|
||||
socket.emit("setFlowData", { key: "setAssetsImage", value: obj });
|
||||
});
|
||||
//通知前端更新状态
|
||||
socket.emit("setFlowData", { key: "setAssetsImage", value: { ...item, id: item.assetId, src: "", state: "生成中" } });
|
||||
}
|
||||
return "资产生成中";
|
||||
generate_storyboard: tool({
|
||||
description: "生成分镜",
|
||||
inputSchema: z.object({}),
|
||||
execute: async ({ script }) => {
|
||||
const thinking = msg.thinking("正在生成分镜...");
|
||||
const res = await new Promise((resolve) => socket.emit("generateStoryboard", { script }, (res: any) => resolve(res)));
|
||||
thinking.appendText("生成的分镜数据:\n" + JSON.stringify(res, null, 2));
|
||||
thinking.updateTitle("分镜生成完成");
|
||||
thinking.complete();
|
||||
return res;
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
return toolsNames ? Object.fromEntries(Object.entries(tools).filter(([n]) => toolsNames.includes(n))) : tools;
|
||||
};
|
||||
|
||||
// 获取资产图片base64
|
||||
async function getAssetsImageBase64(imageIds: number[]) {
|
||||
if (imageIds.length === 0) return [];
|
||||
const imagePaths = await u
|
||||
.db("o_assets")
|
||||
.leftJoin("o_image", "o_assets.imageId", "o_image.id")
|
||||
.whereIn("o_assets.id", imageIds)
|
||||
.select("o_assets.id", "o_image.filePath");
|
||||
if (!imagePaths.length) return [];
|
||||
const imageUrls = await Promise.all(
|
||||
imagePaths.map(async (i) => {
|
||||
if (i.filePath) {
|
||||
try {
|
||||
return await urlToBase64(await u.oss.getFileUrl(i.filePath));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
return imageUrls.filter(Boolean) as string[];
|
||||
}
|
||||
|
||||
//获取分镜图片base64
|
||||
async function getStoryboardImageBase64(imageIds: number[]) {
|
||||
if (!imageIds.length) return [];
|
||||
const storayboardData = await u.db("o_storyboard").whereIn("id", imageIds).select("id", "filePath");
|
||||
const imageUrls = await Promise.all(
|
||||
storayboardData.map(async (i) => {
|
||||
if (i.filePath) {
|
||||
try {
|
||||
return await urlToBase64(await u.oss.getFileUrl(i.filePath));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
return imageUrls.filter(Boolean) as string[];
|
||||
}
|
||||
|
||||
@ -130,7 +130,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
|
||||
},
|
||||
{
|
||||
key: "messagesPerSummary",
|
||||
value: 3,
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
key: "shortTermLimit",
|
||||
|
||||
@ -49,7 +49,7 @@ export default (nsp: Namespace) => {
|
||||
const currentController = abortController;
|
||||
const memory = new Memory("scriptAgent", isolationKey);
|
||||
|
||||
const msg = resTool.newMessage("assistant", "统筹");
|
||||
const msg = resTool.newMessage("assistant", "视频策划");
|
||||
const ctx: agent.AgentContext = {
|
||||
socket,
|
||||
isolationKey,
|
||||
@ -69,7 +69,7 @@ export default (nsp: Namespace) => {
|
||||
const persistCurrentMessage = async () => {
|
||||
if (!currentContent.trim()) return;
|
||||
await memory.add("assistant:decision", currentContent, {
|
||||
name: "统筹",
|
||||
name: "视频策划",
|
||||
createTime: new Date(currentMsg.datetime).getTime(),
|
||||
});
|
||||
currentContent = "";
|
||||
@ -103,14 +103,6 @@ export default (nsp: Namespace) => {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("setModelData", async (data: any) => {
|
||||
resTool.data.imageModel = data;
|
||||
});
|
||||
socket.on("setKeyScript", async (data: any) => {
|
||||
isolationKey = data.key;
|
||||
resTool.data.scriptId = data.scriptId;
|
||||
});
|
||||
|
||||
socket.on("stop", () => {
|
||||
abortController?.abort();
|
||||
abortController = null;
|
||||
|
||||
33
src/types/database.d.ts
vendored
33
src/types/database.d.ts
vendored
@ -1,19 +1,19 @@
|
||||
// @db-hash 06e91b1ef334867ed5ea41d5a857d07a
|
||||
<<<<<<< HEAD
|
||||
// @db-hash 93b2462070c45c2b449e9a18c4e88763
|
||||
//该文件由脚本自动生成,请勿手动修改
|
||||
|
||||
export interface _o_project_old_20260328 {
|
||||
'artStyle'?: string | null;
|
||||
=======
|
||||
// @db-hash f7bc2fdb80756d5536929eb47155578b
|
||||
//该文件由脚本自动生成,请勿手动修改
|
||||
|
||||
export interface _o_script_old_20260327 {
|
||||
'content'?: string | null;
|
||||
'createTime'?: number | null;
|
||||
'id'?: number | null;
|
||||
'imageModel'?: string | null;
|
||||
'intro'?: string | null;
|
||||
'id'?: number;
|
||||
'name'?: string | null;
|
||||
'projectType'?: string | null;
|
||||
'type'?: string | null;
|
||||
'userId'?: number | null;
|
||||
'videoModel'?: string | null;
|
||||
'videoRatio'?: string | null;
|
||||
'projectId'?: number | null;
|
||||
}
|
||||
>>>>>>> 9da2610cdedc1e293b351ed3ab67fbc6fcd989f1
|
||||
export interface memories {
|
||||
'content': string;
|
||||
'createTime': number;
|
||||
@ -34,7 +34,7 @@ export interface o_agentDeploy {
|
||||
'model'?: string | null;
|
||||
'modelName'?: string | null;
|
||||
'name'?: string | null;
|
||||
'vendorId'?: number | null;
|
||||
'vendorId'?: string | null;
|
||||
}
|
||||
export interface o_agentWorkData {
|
||||
'createTime'?: number | null;
|
||||
@ -60,7 +60,6 @@ export interface o_assets {
|
||||
'name'?: string | null;
|
||||
'projectId'?: number | null;
|
||||
'prompt'?: string | null;
|
||||
'promptState'?: string | null;
|
||||
'remark'?: string | null;
|
||||
'scriptId'?: number | null;
|
||||
'startTime'?: number | null;
|
||||
@ -180,7 +179,7 @@ export interface o_storyboard {
|
||||
'filePath'?: string | null;
|
||||
'frameMode'?: string | null;
|
||||
'id'?: number;
|
||||
'index'?: number | null;
|
||||
'index'?: string | null;
|
||||
'lines'?: string | null;
|
||||
'mode'?: string | null;
|
||||
'model'?: string | null;
|
||||
@ -191,7 +190,6 @@ export interface o_storyboard {
|
||||
'sound'?: string | null;
|
||||
'state'?: string | null;
|
||||
'title'?: string | null;
|
||||
'videoPrompt'?: string | null;
|
||||
}
|
||||
export interface o_tasks {
|
||||
'describe'?: string | null;
|
||||
@ -246,7 +244,10 @@ export interface o_videoConfig {
|
||||
}
|
||||
|
||||
export interface DB {
|
||||
"_o_project_old_20260328": _o_project_old_20260328;
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"_o_script_old_20260327": _o_script_old_20260327;
|
||||
>>>>>>> 9da2610cdedc1e293b351ed3ab67fbc6fcd989f1
|
||||
"memories": memories;
|
||||
"o_agentDeploy": o_agentDeploy;
|
||||
"o_agentWorkData": o_agentWorkData;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user