diff --git a/README.md b/README.md index e640e25..e862983 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ docker-compose -f docker/docker-compose.local.yml up -d --build | 端口 | 用途 | 在线部署映射 | 本地构建映射 | | ------- | -------------- | ------------- | ------------- | | `80` | Nginx 前端页面 | 随机端口 | `8080:80` | -| `60000` | 后端 API 服务 | `60000:60000` | `60000:60000` | +| `10588` | 后端 API 服务 | `10588:10588` | `10588:10588` | ### 数据持久化 @@ -265,8 +265,8 @@ yarn build "exec_mode": "cluster", "env": { "NODE_ENV": "prod", - "PORT": 60000, - "OSSURL": "http://127.0.0.1:60000/" + "PORT": 10588, + "OSSURL": "http://127.0.0.1:10588/" } } ``` @@ -361,7 +361,7 @@ pm2 monit # 监控面板 yarn dev ``` - > ⚠️ 此命令仅启动后端 API 服务(端口 60000),**不包含前端页面**。直接访问 `http://localhost:60000` 只能调用 API 接口,无法看到完整的网页界面。如需同时使用前端页面,请配合前端项目单独启动,或使用下方的 GUI 模式。 + > ⚠️ 此命令仅启动后端 API 服务(端口 10588),**不包含前端页面**。直接访问 `http://localhost:10588` 只能调用 API 接口,无法看到完整的网页界面。如需同时使用前端页面,请配合前端项目单独启动,或使用下方的 GUI 模式。 - **方式二:启动 Electron 桌面客户端(推荐完整体验)** @@ -375,7 +375,7 @@ pm2 monit # 监控面板 | 命令 | 启动内容 | 前端页面 | 适用场景 | | -------------- | ------------------------ | -------- | -------------------------------- | - | `yarn dev` | 仅后端 API(端口 60000) | ❌ 无 | 后端开发调试、配合前端项目联调 | + | `yarn dev` | 仅后端 API(端口 10588) | ❌ 无 | 后端开发调试、配合前端项目联调 | | `yarn dev:gui` | 后端 + Electron 桌面端 | ✅ 内置 | 完整功能体验、桌面客户端开发调试 | 4. **项目打包** diff --git a/data/skills/production-agent/decision/SKILL.md b/data/skills/production-agent/decision/SKILL.md index 73da9d7..1226750 100644 --- a/data/skills/production-agent/decision/SKILL.md +++ b/data/skills/production-agent/decision/SKILL.md @@ -5,21 +5,21 @@ description: 短剧漫剧制作决策层。负责分析用户需求、制定执 # Decision Agent -短剧漫剧制作的指挥层,负责整体决策和协调。接收用户需求后,先制定计划、获得用户确认,再将任务逐步交给执行层完成。 +短剧漫剧制作的指挥层,负责整体决策和协调。始终以用户当前指令为最终目标推进:默认直接协调执行,只有用户明确提出需要新增或修改拍摄计划时,才进入计划编辑与确认流程。 ## 可用工具 -| 工具 | 说明 | -|------|------| -| `activate_skill` | 激活技能,加载完整指令和资源列表到上下文 | -| `read_skill_file` | 读取已激活技能目录下的参考资料文件 | -| `deepRetrieve` | 深度检索记忆,通过关键词回忆历史对话详情 | -| `run_sub_agent` | 启动子Agent执行任务(可用:`executionAI`、`supervisionAI`) | -| `get_flowData` | 获取工作区数据(key: `script` 剧本 / `assets` 资产列表) | -| `get_flowData_schema` | 获取工作区数据的类型结构 | -| `set_flowData` | 保存数据到工作区(lodash 路径) | -| `generate_assets_images` | 生成衍生资产图片(传入资产 id 列表) | -| `generate_storyboard_images` | 生成分镜图(传入剧本文本) | +| 工具 | 说明 | +| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `activate_skill` | 激活技能,加载完整指令和资源列表到上下文 | +| `read_skill_file` | 读取已激活技能目录下的参考资料文件 | +| `deepRetrieve` | 深度检索记忆,通过关键词回忆历史对话详情 | +| `run_sub_agent` | 启动子 Agent 执行任务(可用:`executionAI`、`supervisionAI`) | +| `get_flowData` | 获取工作区数据(key: `script` 剧本 / `scriptPlan` 拍摄计划 / `assets` 资产列表 / `storyboardTable` 分镜表) | +| `get_flowData_schema` | 获取工作区数据的类型结构 | +| `set_flowData` | 保存数据到工作区(lodash 路径) | +| `generate_assets_images` | 生成衍生资产图片(传入资产 id 列表) | +| `generate_storyboard_images` | 生成分镜图(传入剧本文本) | ## 核心工作流程(必须严格遵循) @@ -27,30 +27,37 @@ description: 短剧漫剧制作决策层。负责分析用户需求、制定执 收到用户消息时,**先判断当前处于哪个阶段**,再决定下一步动作: -- **用户发起新的制作任务**(如"开始制作第4集"、"帮我拆分剧本"等明确的新需求) → 进入阶段一 -- **用户确认计划**(如"可以"、"确认"、"开始吧"、"没问题"等) → 直接进入阶段三执行,**不要重新制定计划** -- **用户要求修改计划**(如"第2步改一下"、"加一个步骤"等) → 留在阶段二,修改后重新回复计划 +- **用户发起执行类需求**(如"开始制作第 4 集"、"继续生成分镜"、"提取角色资产") → 直接进入阶段三,按用户目标执行 +- **用户明确要求新增/修改拍摄计划**(如"给我出一版拍摄计划"、"第 2 步改一下"、"加一个镜头") → 进入阶段二,更新 `scriptPlan` 并与用户确认 +- **用户确认拍摄计划**(如"可以"、"确认"、"开始吧"、"没问题") → 在不重做计划的前提下进入阶段三执行 -**禁止**:把用户的确认或简短回复当作新任务重新走阶段一。 +**禁止**:在用户未提出计划诉求时,主动生成或反复重生成拍摄计划。 -### 阶段一:收集信息(仅新任务触发) +### 阶段一:收集信息(仅首次进入会话或上下文不足时触发) -1. 调用 `get_flowData` 获取当前工作区的剧本和资产数据,了解项目现状 -2. 调用 `deepRetrieve` 检索相关历史记忆,了解已完成的工作进度 -3. 使用 `read_skill_file` 加载 `references/plan.md` 获取计划制定规范 +1. 调用 `get_flowData`(key: `script`)获取当前剧本内容 +2. 调用 `get_flowData`(key: `scriptPlan`)获取已有拍摄计划(可能为空) +3. 调用 `get_flowData`(key: `assets`)获取资产数据,了解项目现状 +4. 调用 `deepRetrieve` 检索相关历史记忆,了解已完成的工作进度 +5. 使用 `read_skill_file` 加载 `references/plan.md` 获取计划制定规范 -### 阶段二:制定计划并确认 +### 阶段二:编辑拍摄计划并对话确认(仅用户明确提出时触发) -1. 结合工作区数据、历史记忆和用户需求,按照 `plan.md` 的规范生成**结构化执行计划** -2. **将计划回复给用户**,请求确认 -3. 如果用户要求调整,修改计划后重新回复,直到用户确认 -4. 输出计划后**停止并等待用户回复**,不要自行继续 +1. 根据剧本内容、工作区数据、历史记忆和用户需求,新增或修改**拍摄计划**(scriptPlan) +2. 调用 `set_flowData`(key: `scriptPlan`, value: 最新拍摄计划文本)将拍摄计划同步到前端工作区 +3. **将拍摄计划回复给用户**,请求确认 +4. 输出拍摄计划后**停止并等待用户回复**,不要自行继续 +5. 如果用户要求调整: + - 根据用户反馈修改拍摄计划 + - 再次调用 `set_flowData`(key: `scriptPlan`, value: 修改后的拍摄计划)同步到前端 + - 重新回复修改后的拍摄计划,继续等待确认 + - **循环此过程**,直到用户明确确认 -### 阶段三:按计划执行(仅用户确认后触发) +### 阶段三:按用户目标执行(默认阶段) -用户确认后,按步骤顺序逐步调用 `run_sub_agent` 工具: +以用户当前指令为目标,优先执行用户要求;若 `scriptPlan` 已存在则按其作为参考,不存在时也可直接执行当前任务。需要分步时再拆解为执行步骤,并按顺序调用 `run_sub_agent` 工具: -1. 每次调用 `run_sub_agent` 时,选择 `executionAI` 作为子Agent,将当前步骤的任务描述作为 `prompt` 参数传入 +1. 每次调用 `run_sub_agent` 时,选择 `executionAI` 作为子 Agent,将当前步骤的任务描述作为 `prompt` 参数传入 2. 检查返回结果是否符合预期,不符合则调整指令重试 3. 将上一步的输出作为上下文传入下一步(如有依赖) 4. 全部步骤完成后,向用户汇报整体结果 @@ -61,7 +68,10 @@ description: 短剧漫剧制作决策层。负责分析用户需求、制定执 - 复杂任务拆分为可独立执行的小步骤 - 关注步骤间的依赖关系,确保顺序合理 - 利用 `deepRetrieve` 检索历史记忆,避免重复已完成的工作 +- **用户目标优先**:默认直接响应并推进用户当前任务,不要为了流程完整性而强制先生成计划 +- **计划按需维护**:仅当用户明确要求新增/修改拍摄计划时,才更新 `scriptPlan`,且每次改动都调用 `set_flowData` 同步到前端 - **提取衍生资产后**:计划中必须包含"询问用户是否生成资产图片"步骤。若用户确认,执行层将调用 `generate_assets_images` 工具批量生成衍生资产图片 +- **生成分镜表后**:计划中必须包含"询问用户是否生成分镜图片"步骤。若用户确认,执行层将调用 `generate_storyboard_images` 工具生成分镜图 ## 参考资料 diff --git a/data/skills/production-agent/execution/SKILL.md b/data/skills/production-agent/execution/SKILL.md index 681fad2..c89abc8 100644 --- a/data/skills/production-agent/execution/SKILL.md +++ b/data/skills/production-agent/execution/SKILL.md @@ -1,7 +1,7 @@ --- name: execution description: > - 用户需要拆分剧本或提取衍生资产时可以看此skill的参考资料,了解拆分原则、衍生资产提取原则和示例 + 用户需要拆分剧本、提取衍生资产或生成分镜表时可以看此skill的参考资料,了解拆分原则、衍生资产提取原则、分镜表生成规范和示例 --- # execution Agent @@ -14,6 +14,7 @@ description: > - 拆分剧本 - 提取衍生资产(从剧本和已有角色资产中提取关联道具、场景物件等衍生资产) +- 生成分镜表(根据剧本和资产生成结构化的分镜表) ## 工作指引 @@ -28,10 +29,21 @@ description: > - 如果用户拒绝,跳过此步骤,流程结束 - 生成图片为异步操作,可以先回复用户"正在生成图片,稍后会自动更新",等图片生成完成后再通知用户查看 +### 生成分镜表流程 + +1. 调用 `get_flowData` 分别获取 `script`(剧本)和 `assets`(现有资产列表) +2. 根据[分镜表生成](references/storyboard-generation.md)文档中的拆分原则和字段填写指引,将剧本拆分为分镜,填写每条分镜的所有字段(id、title、description、camera、duration、frameMode、prompt、lines、sound、associateAssetsIds) +3. 调用 `set_flowData({ key: "storyboardTable", value: 分镜数组 })` 一次性保存完整分镜表 +4. 告知用户分镜表生成完成,列出分镜概要(总条数、主要场景) +5. **询问用户是否需要生成分镜图片**: + - 如果用户确认需要,调用 `generate_storyboard_images({ script: 剧本文本 })` 生成分镜图 + - 如果用户拒绝,跳过此步骤,流程结束 + ## 参考资料 本技能附带以下参考资料,根据任务需要使用 `read_skill_file` 工具按需加载: - [衍生资产提取](references/derive-assets-extraction.md) — 从剧本和角色资产中提取衍生资产的原则和示例 +- [分镜表生成](references/storyboard-generation.md) — 从剧本和资产生成分镜表的拆分原则、字段规范和示例 **注意**:根据用户当前任务选择性加载对应参考资料,不要一次性全部加载。 diff --git a/data/skills/production-agent/execution/references/storyboard-generation.md b/data/skills/production-agent/execution/references/storyboard-generation.md new file mode 100644 index 0000000..8f82f2b --- /dev/null +++ b/data/skills/production-agent/execution/references/storyboard-generation.md @@ -0,0 +1,248 @@ +# 分镜表生成(从剧本 + 资产 → storyboardTable) + +本指南只做一件事: +根据剧本内容和已有资产,将剧本拆分为一系列分镜,生成结构化的分镜表。 + +> **核心概念**:分镜表是将剧本转化为视觉画面的中间产物。每条分镜对应一个独立的画面/镜头,包含画面描述、镜头语言、台词、音效和关联资产等信息,用于后续图片生成。 + +## 1. 输入与输出 + +### 输入 + +- 剧本文本(字符串),通过 `get_flowData("script")` 获取 +- 已有资产列表(数组),通过 `get_flowData("assets")` 获取 + +### 输出 + +调用 `set_flowData` 将分镜表写入工作区: + +```ts +set_flowData({ + key: "storyboardTable", + value: [ + { + id: 1, + title: "分镜标题", + description: "画面描述", + camera: "镜头语言", + duration: 3, + frameMode: "firstFrame", + prompt: "图片生成提示词", + lines: "台词文本", + sound: "音效描述", + associateAssetsIds: [0, 2] + }, + // ...更多分镜 + ] +}) +``` + +### 字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | number | 分镜序号,从 1 开始递增 | +| `title` | string | 分镜标题,简明概括画面内容(2~10字) | +| `description` | string | 画面描述,描述画面中发生的事件和视觉元素 | +| `camera` | string | 镜头语言,描述镜头角度、运动方式 | +| `duration` | number | 画面持续时长(秒),根据内容复杂度和节奏估算 | +| `frameMode` | enum | 帧模式:`firstFrame`(首帧)/ `endFrame`(尾帧)/ `linesSoundEffects`(台词音效帧) | +| `prompt` | string | 图片生成提示词,用于 AI 绘图的英文提示词 | +| `lines` | string \| null | 台词,该分镜中角色说的话,无台词填 `null` | +| `sound` | string \| null | 音效描述,该分镜中的环境音/音效,无音效填 `null` | +| `associateAssetsIds` | number[] | 关联资产的索引(对应 assets 数组的下标),标注该分镜画面中出现的资产 | + +## 2. 分镜拆分原则 + +### 2.1 拆分粒度 + +- **一个独立画面 = 一条分镜**:画面主体、场景或视角发生明显变化时,新起一条分镜 +- 同一段对话如果镜头在不同角色间切换,每个镜头视角单独拆分 +- 动作场景按关键动作节点拆分,不要把整段打戏塞进一条分镜 +- 过渡/转场单独拆分为一条分镜(如果有明确的过渡描写) + +### 2.2 拆分判断标准 + +新起一条分镜的信号: +- 场景/地点切换 +- 时间跳跃 +- 镜头主体切换(从角色 A 切到角色 B) +- 同一角色的视角/景别明显变化(远景 → 特写) +- 重要动作或事件节点 + +不需要新起分镜的情况: +- 同一画面内的连续对话(可合并到一条分镜) +- 表情微变或小动作(可在描述中囊括) + +## 3. 各字段填写指引 + +### 3.1 title(分镜标题) + +- 2~10 个字,概括核心画面内容 +- 格式:`[主体] + [动作/状态]` +- 示例:"凌玄吐血"、"青云令碎裂"、"宗门远景"、"苏晚卿冷笑" + +### 3.2 description(画面描述) + +- 描述画面中**可见的**视觉内容,不要写心理活动 +- 包含:人物动作、表情、环境状态、关键物件 +- 20~80 字为宜 +- 示例:"凌玄跪在大殿地面上,鲜血从嘴角溢出,右手死死攥住已经裂开的青云令,面色苍白" + +### 3.3 camera(镜头语言) + +常用镜头语言参考: + +| 景别 | 说明 | +|------|------| +| 大远景 | 展示环境全貌,人物极小 | +| 远景 | 展示场景与人物关系 | +| 全景 | 展示人物全身与周围环境 | +| 中景 | 人物膝盖以上 | +| 近景 | 人物胸部以上 | +| 特写 | 面部或物件局部放大 | +| 大特写 | 眼睛、手等极致局部 | + +常用运镜: +- 推镜头:从远到近,强调主体 +- 拉镜头:从近到远,展示环境 +- 摇镜头:镜头固定位置旋转,扫视场景 +- 移镜头:镜头跟随主体移动 +- 俯拍:从上往下拍 +- 仰拍:从下往上拍 + +格式:`[景别] · [运镜]`(运镜非必须) +示例:"特写"、"近景 · 缓慢推进"、"大远景 · 俯拍"、"中景 · 跟随移动" + +### 3.4 duration(时长) + +根据内容估算画面持续时间(秒): +- 静态画面/特写:2~3 秒 +- 对话镜头:根据台词长度,约 3~6 秒 +- 动作场景:2~4 秒 +- 环境全景/过渡:2~4 秒 +- 复杂场景:5~8 秒 + +### 3.5 frameMode(帧模式) + +根据分镜内容选择合适的帧模式: + +| 模式 | 使用场景 | +|------|----------| +| `firstFrame` | 最常见。画面以**起始状态**为主,如角色站立、场景展示、动作起始瞬间 | +| `endFrame` | 画面以**结束状态**为主,如打击命中瞬间、物件破碎后、倒地后 | +| `linesSoundEffects` | 画面以**台词或音效**为主,画面本身变化不大,重点在声音内容 | + +### 3.6 prompt(图片生成提示词) + +- **必须使用英文** +- 描述画面的视觉内容,包含人物外观、动作、场景、光影、氛围等 +- 可以参考关联资产的 `desc` 来描述人物/物件的外观特征 +- 不要包含剧情叙事或对话内容 +- 格式建议:`[主体描述], [动作/姿态], [场景/背景], [光影/氛围], [风格/画质关键词]` +- 示例:"A young man in white robes kneeling on the ground of a grand hall, blood dripping from his mouth, clenching a cracked jade token, pale face, dramatic lighting, cinematic composition" + +### 3.7 lines(台词) + +- 该分镜中角色说的台词,直接提取剧本原文 +- 如有多个角色说话,按顺序排列,格式:`角色名:台词内容` +- 无台词的分镜填 `null` + +### 3.8 sound(音效) + +- 描述该分镜中需要的音效或环境声 +- 示例:"剑鸣声"、"风声呼啸"、"玉石碎裂声"、"人群惊呼" +- 无特殊音效填 `null` + +### 3.9 associateAssetsIds(关联资产) + +- 填写该分镜画面中**出现的资产**在 assets 数组中的**索引**(从 0 开始) +- 只关联画面中**可见的**资产,不关联仅被提及但不在画面中的资产 +- 示例:如果 assets[0] 是"凌玄"、assets[2] 是"青云令",且这两个都出现在画面中,则填 `[0, 2]` + +## 4. 示例 + +### 输入剧本片段 + +``` +苏晚卿冷笑:「还有你当宝贝的青云令」 +「若不是我趁你养伤时,偷偷在令牌上动了手脚」 +△ 凌玄气血逆流,再次一口鲜血喷出 +△ 青云令表面灵纹暗淡,隐约可见细微裂痕 +``` + +### 输入资产 + +```json +[ + { "assetsId": "char-1", "name": "凌玄", "desc": "男主 · 青云宗宗主 · 白发修长 · 身着白色宗主袍" }, + { "assetsId": "char-2", "name": "苏晚卿", "desc": "女配 · 凌玄未婚妻 · 红衣 · 冷艳" }, + { "assetsId": "item-1", "name": "青云令", "desc": "宗主信物 · 青玉材质 · 灵纹浮刻" } +] +``` + +### 输出 + +```ts +set_flowData({ + key: "storyboardTable", + value: [ + { + id: 1, + title: "苏晚卿冷笑", + description: "苏晚卿站在大殿中,嘴角勾起冷笑,目光居高临下看着跪在地上的凌玄", + camera: "近景", + duration: 4, + frameMode: "linesSoundEffects", + prompt: "A beautiful woman in red robes standing in a grand hall, cold smirk on her face, looking down at someone, dramatic indoor lighting, cinematic", + lines: "苏晚卿:还有你当宝贝的青云令,若不是我趁你养伤时,偷偷在令牌上动了手脚", + sound: null, + associateAssetsIds: [1] + }, + { + id: 2, + title: "凌玄吐血", + description: "凌玄气血逆流,猛然喷出一口鲜血,身体摇摇欲坠", + camera: "中景 · 缓慢推进", + duration: 3, + frameMode: "endFrame", + prompt: "A white-haired young man in white robes kneeling on the floor, spitting blood, trembling body, pale face, dramatic lighting, cinematic composition", + lines: null, + sound: "喷血声", + associateAssetsIds: [0] + }, + { + id: 3, + title: "青云令裂痕", + description: "青云令表面灵纹逐渐暗淡,青玉上浮现细微裂痕", + camera: "大特写", + duration: 3, + frameMode: "firstFrame", + prompt: "Close-up of a jade token with glowing runes fading, fine cracks appearing on the surface, dark moody lighting, cinematic detail shot", + lines: null, + sound: "玉石碎裂声", + associateAssetsIds: [2] + } + ] +}) +``` + +## 5. 工具调用顺序 + +1. `get_flowData("script")` — 获取剧本内容 +2. `get_flowData("assets")` — 获取已有资产列表 +3. 分析剧本,按照拆分原则划分分镜,并为每条分镜填写所有字段 +4. 调用 `set_flowData({ key: "storyboardTable", value: 分镜数组 })` 一次性保存完整分镜表 +5. 向用户汇报分镜表概要(总共多少条分镜,覆盖的场景概括) +6. **询问用户是否需要生成分镜图片**: + - 如果用户确认,调用 `generate_storyboard_images({ script: 剧本文本 })` 生成分镜图 + - 如果用户拒绝,跳过此步骤,流程结束 + +## 6. 注意事项 + +- 分镜数量与剧本长度成正比,一般每 50~100 字剧本对应 1~2 条分镜 +- prompt 必须使用英文,且只描述视觉内容 +- `associateAssetsIds` 使用资产数组的索引(0-based),确保索引不越界 +- 如果剧本中出现了资产列表中不存在的角色/物件,仍要在分镜中描述,但不要在 `associateAssetsIds` 中编造不存在的索引 +- 分镜的顺序应与剧本的叙事顺序一致 +- 合理使用三种 frameMode,大部分分镜使用 `firstFrame`,涉及动作结果的用 `endFrame`,以对话为主的用 `linesSoundEffects` diff --git a/docker/Dockerfile b/docker/Dockerfile index bd7c07c..71c4751 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -114,7 +114,7 @@ EOF ENV NODE_ENV=prod EXPOSE 80 -EXPOSE 60000 +EXPOSE 10588 # 启动时创建必要目录(防止 volume 挂载覆盖) CMD sh -c "mkdir -p /var/log/nginx /var/lib/nginx/logs && exec supervisord -c /etc/supervisord.conf" diff --git a/docker/Dockerfile.local b/docker/Dockerfile.local index 3745495..d1cf439 100644 --- a/docker/Dockerfile.local +++ b/docker/Dockerfile.local @@ -88,7 +88,7 @@ EOF ENV NODE_ENV=prod EXPOSE 80 -EXPOSE 60000 +EXPOSE 10588 # 启动时创建必要目录(防止 volume 挂载覆盖) CMD sh -c "mkdir -p /var/log/nginx /var/lib/nginx/logs && exec supervisord -c /etc/supervisord.conf" diff --git a/docker/docker-compose.local.yml b/docker/docker-compose.local.yml index cf54b76..42923f6 100644 --- a/docker/docker-compose.local.yml +++ b/docker/docker-compose.local.yml @@ -11,7 +11,7 @@ services: restart: unless-stopped ports: - "8080:80" - - "60000:60000" + - "10588:10588" environment: - NODE_ENV=prod volumes: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d70050c..d8f4b1e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -12,7 +12,7 @@ services: restart: unless-stopped ports: - "80" - - "60000:60000" + - "10588:10588" environment: - NODE_ENV=prod volumes: diff --git a/docs/README.en.md b/docs/README.en.md index 6925d37..2c8e007 100644 --- a/docs/README.en.md +++ b/docs/README.en.md @@ -175,8 +175,8 @@ Create a `pm2.json` file: "exec_mode": "cluster", "env": { "NODE_ENV": "prod", - "PORT": 60000, - "OSSURL": "http://127.0.0.1:60000/" + "PORT": 10588, + "OSSURL": "http://127.0.0.1:10588/" } } ``` @@ -257,13 +257,13 @@ To deploy or customize the frontend separately, refer to the frontend repo: - Launch dev server with Node.js: ```bash - yarn dev #port 60000 + yarn dev #port 10588 ``` - Use Bun to quickly start dev server: ```bash - yarn bun:dev #port 60000 + yarn bun:dev #port 10588 ``` 4. **Build the Project** diff --git a/env/.env.dev b/env/.env.dev index b8dd44a..584000d 100644 --- a/env/.env.dev +++ b/env/.env.dev @@ -1,4 +1,4 @@ NODE_ENV=dev -PORT=60000 -OSSURL=http://127.0.0.1:60000/ +PORT=10588 +OSSURL=http://127.0.0.1:10588/ diff --git a/env/.env.prod b/env/.env.prod index bbf0003..db28ae4 100644 --- a/env/.env.prod +++ b/env/.env.prod @@ -1,4 +1,4 @@ NODE_ENV=prod -PORT=60000 -OSSURL=http://127.0.0.1:60000/ +PORT=10588 +OSSURL=http://127.0.0.1:10588/ diff --git a/scripts/build.ts b/scripts/build.ts index 2282b29..b1412e8 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -14,7 +14,7 @@ if (!fs.existsSync(envDir)) { fs.mkdirSync(envDir, { recursive: true }); } if (!fs.existsSync(envFile)) { - const defaultEnv = `NODE_ENV=${process.env.NODE_ENV}\nPORT=60000\nOSSURL=http://127.0.0.1:60000/\n`; + const defaultEnv = `NODE_ENV=${process.env.NODE_ENV}\nPORT=10588\nOSSURL=http://127.0.0.1:10588/\n`; fs.writeFileSync(envFile, defaultEnv, "utf8"); console.log(`📄 已自动创建环境变量文件: ${envFile}`); } diff --git a/scripts/main.ts b/scripts/main.ts index d39b1c9..4394de5 100644 --- a/scripts/main.ts +++ b/scripts/main.ts @@ -4,7 +4,7 @@ import startServe, { closeServe } from "src/app"; import { number } from "zod"; // 默认端口配置 -const defaultPort = 60000; +const defaultPort = 10588; function createMainWindow(port: any): void { const win = new BrowserWindow({ @@ -35,7 +35,7 @@ function createMainWindow(port: any): void { app.whenReady().then(async () => { try { const port = await startServe(false); - createMainWindow(60000); + createMainWindow(10588); } catch (err) { console.error("[服务启动失败]:", err); // 如果服务启动失败,使用默认端口创建窗口 diff --git a/scripts/web/index.html b/scripts/web/index.html index 2aee926..c245756 100644 --- a/scripts/web/index.html +++ b/scripts/web/index.html @@ -1,10 +1,10 @@ - - - - - - - Toonflow + + + + + + + Toonflow - - - -
- - + + + +
+ + + diff --git a/src/agents/productionAgent/index.ts b/src/agents/productionAgent/index.ts index e9340f5..c8f2cc7 100644 --- a/src/agents/productionAgent/index.ts +++ b/src/agents/productionAgent/index.ts @@ -42,7 +42,7 @@ export async function decisionAI(ctx: AgentContext) { const systemPrompt = buildSystemPrompt(skill.prompt, mem); - const prefixSystem = `请回复用户收到以后直接调用run_sub_agent运行**executionAI**执行用户的任务`; + const prefixSystem = `以用户当前指令为最终目标。默认直接推进执行;仅当用户明确要求新增或修改拍摄计划时,才调用set_flowData更新scriptPlan并与用户确认。需要执行任务时调用run_sub_agent运行**executionAI**。`; const { textStream } = await u.Ai.Text("productionAgent").stream({ system: prefixSystem + systemPrompt, diff --git a/src/agents/productionAgent/tools.ts b/src/agents/productionAgent/tools.ts index d37fd8d..d5fd9ae 100644 --- a/src/agents/productionAgent/tools.ts +++ b/src/agents/productionAgent/tools.ts @@ -5,24 +5,16 @@ import { Socket } from "socket.io"; const deriveSchema = z.object({ name: z.string().min(1).max(20), desc: z.string().min(1).max(100) }); const assetSchema = z.object({ assetsId: z.string(), name: z.string(), desc: z.string(), src: z.string(), derive: z.array(deriveSchema).optional() }); -const storyboardTableSchema = z.array( - z.object({ - id: z.number(), - title: z.string(), - description: z.string(), - camera: z.string(), - duration: z.number(), - frameMode: z.enum(["firstFrame", "endFrame", "linesSoundEffects"]), - lines: z.string().nullable(), - sound: z.string().nullable(), - associateAssetsIds: z.array(z.number()), - }), -); -const flowDataSchema = z.object({ script: z.string(), assets: z.array(assetSchema), storyboardTable: storyboardTableSchema }); +const storyboardTableSchema = z.string().describe("分镜表的markdown文本"); +const flowDataSchema = z.object({ script: z.string(), scriptPlan: z.string(), assets: z.array(assetSchema), storyboardTable: storyboardTableSchema }); type FlowData = z.infer; -const keySchema = z.object({ key: z.enum(["script", "assets", "storyboardTable"]).describe("script=剧本,assets=资产列表,storyboardTable=分镜表") }); +const keySchema = z.object({ + key: z + .enum(["script", "scriptPlan", "assets", "storyboardTable"]) + .describe("script=剧本,scriptPlan=拍摄计划,assets=资产列表,storyboardTable=分镜表"), +}); const valueSchema = z .union([z.string(), z.array(assetSchema), assetSchema, z.array(deriveSchema), z.array(storyboardTableSchema)]) .describe("路径对应的值"); @@ -66,20 +58,48 @@ export default (socket: Socket, toolsNames?: string[]) => { return true; }, }), - generate_assets_images: tool({ - description: "生成衍生资产的图片", - inputSchema: z.object({ ids: z.array(z.string()).describe("需要生成的资产id列表") }), - execute: async ({ ids }) => { - console.log("[tools] generated_assets", ids); - return new Promise((resolve) => socket.emit("generatedAssets", { ids }, (res: any) => resolve(res))); + + generate_storyboard_images: tool({ + description: `生成一组图片任务,支持图片间的依赖关系(以图生图)。 + +参数说明: +- images: 图片任务数组 + - id: 图片唯一标识符 + - prompt: 图片生成提示词 + - referenceIds: 依赖的参考图id数组,无依赖填空数组[] + - assetIds: 参考的资产图id数组(可选) + +依赖规则: +1. referenceIds中的id必须存在于images数组中 +2. 禁止循环依赖(如A依赖B,B依赖A) +3. 被依赖的图片会先生成,其结果作为参考图传入 + +示例:生成猫图,再以猫图为参考生成狗图 +images: [ + {id: "cat", prompt: "一只橘猫", referenceIds: [], assetIds: []}, + {id: "dog", prompt: "风格相同的金毛犬", referenceIds: ["cat"], assetIds: []} +]`, + inputSchema: z.object({ + images: z.array( + z.object({ + id: z.string().describe("图片唯一标识符"), + prompt: z.string().describe("图片生成提示词"), + referenceIds: z.array(z.string()).describe("依赖的参考图id数组,无依赖填空数组[]"), + assetIds: z.array(z.number()).optional().describe("参考的资产图"), + }), + ), + }), + execute: async ({ images }) => { + console.log("[tools] generated_assets", images); + return new Promise((resolve) => socket.emit("generatedAssets", { images }, (res: any) => resolve(res))); }, }), - generate_storyboard_images: tool({ + generate_assets_images: tool({ description: "生成分镜图", - inputSchema: z.object({ script: z.string().describe("剧本文本") }), - execute: async ({ script }) => { - console.log("[tools] generate_storyboard_images", script); - return new Promise((resolve) => socket.emit("generateStoryboardImages", { script }, (res: any) => resolve(res))); + inputSchema: z.object({ images: z.array(z.object({ assetId: z.number(), prompt: z.string() })) }), + execute: async ({ images }) => { + console.log("[tools] generate_assets_images", images); + return new Promise((resolve) => socket.emit("generateAssetsImages", { images }, (res: any) => resolve(res))); }, }), }; diff --git a/src/app.ts b/src/app.ts index 170b444..3458637 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,4 @@ -import "./logger"; +// import "./logger"; import "./err"; import "./env"; import express, { Request, Response, NextFunction } from "express"; @@ -75,7 +75,7 @@ export default async function startServe(randomPort: Boolean = false) { res.status(err.status || 500).send(err); }); - const port = randomPort ? 0 : parseInt(process.env.PORT || "60000"); + const port = randomPort ? 0 : parseInt(process.env.PORT || "10588"); return await new Promise((resolve) => { server.listen(port, async () => { const address = server.address(); diff --git a/src/env.ts b/src/env.ts index 5b209d5..e733138 100644 --- a/src/env.ts +++ b/src/env.ts @@ -3,8 +3,8 @@ import path from "path"; // 默认环境变量(当 env 文件不存在时自动创建) const defaultEnvValues: Record = { - dev: `NODE_ENV=dev\nPORT=60000\nOSSURL=http://127.0.0.1:60000/`, - prod: `NODE_ENV=prod\nPORT=60000\nOSSURL=http://127.0.0.1:60000/`, + dev: `NODE_ENV=dev\nPORT=10588\nOSSURL=http://127.0.0.1:10588/`, + prod: `NODE_ENV=prod\nPORT=10588\nOSSURL=http://127.0.0.1:10588/`, }; // 判断是否为打包后的 Electron 环境 diff --git a/src/routes/production/getStoryboardData.ts b/src/routes/production/getStoryboardData.ts index 4d5dc5d..6a46f69 100644 --- a/src/routes/production/getStoryboardData.ts +++ b/src/routes/production/getStoryboardData.ts @@ -13,7 +13,6 @@ export default router.post( async (req, res) => { const { projectId } = req.body; const storyboardData = await u.db("o_storyboard"); - console.log("%c Line:16 🍖 storyboardData", "background:#ed9ec7", storyboardData); const data = await Promise.all( storyboardData.map(async (i) => { return { diff --git a/src/types/database.d.ts b/src/types/database.d.ts index 775ad75..552d31b 100644 --- a/src/types/database.d.ts +++ b/src/types/database.d.ts @@ -1,4 +1,4 @@ -// @db-hash 8af8e41e3ca0cb5ee554944515d72ba8 +// @db-hash bd46e7c381481a74efedc662a4f9049f //该文件由脚本自动生成,请勿手动修改 export interface memories { @@ -163,13 +163,8 @@ export interface o_skills { } export interface o_storyboard { 'createTime'?: number | null; - 'detail'?: string | null; - 'filePath'?: string | null; - 'frameType'?: string | null; 'id'?: number; 'name'?: string | null; - 'prompt'?: string | null; - 'seconds'?: string | null; } export interface o_storyboardFlow { 'flowData': string; diff --git a/src/utils/oss.ts b/src/utils/oss.ts index d9206d6..ae65c9a 100644 --- a/src/utils/oss.ts +++ b/src/utils/oss.ts @@ -49,7 +49,7 @@ class OSS { await this.ensureInit(); const safePath = normalizeUserPath(userRelPath); // URL 始终使用 /,所以这里需要将系统分隔符转回 / - const url = process.env.OSSURL || `http://127.0.0.1:60000/`; + const url = process.env.OSSURL || `http://127.0.0.1:10588/`; return `${url}${safePath.split(path.sep).join("/")}`; }