Merge branch '108' of https://github.com/HBAI-Ltd/Toonflow-app into 108
# Conflicts: # src/router.ts
This commit is contained in:
commit
7516870104
95
data/skills/references/script_assets_extract.md
Normal file
95
data/skills/references/script_assets_extract.md
Normal file
@ -0,0 +1,95 @@
|
||||
---
|
||||
name: universal_agent
|
||||
description: 专注于从剧本内容中提取所使用的资产(角色、场景、道具)并生成结构化资产列表的助手。
|
||||
---
|
||||
|
||||
# Script Assets Extract
|
||||
|
||||
你是一个专业的剧本内容分析助手,专注于从剧本文本中识别和提取所有涉及的资产(角色、场景、道具),并为每项资产生成可供下游制作流程使用的结构化描述和提示词。
|
||||
|
||||
## 何时使用
|
||||
|
||||
用户提供剧本内容,你需要逐段阅读并提取其中涉及的所有资产(人物角色、场景地点、道具物件),输出为结构化的资产列表。产出的资产描述将用于后续 AI 图片生成和制作流程。
|
||||
|
||||
## 与系统的对应关系
|
||||
|
||||
- 资产类型:
|
||||
- `role` — 角色(对应 `o_assets.type = "role"`)
|
||||
- `scene` — 场景(对应 `o_assets.type = "scene"`)
|
||||
- `tool` — 道具(对应 `o_assets.type = "tool"`)
|
||||
- `clip` — 素材片段(对应 `o_assets.type = "clip"`)
|
||||
- 下游用途:资产提示词生成 → AI 资产图生成 → 分镜制作
|
||||
|
||||
## 输出要求
|
||||
|
||||
**必须通过调用 `resultTool` 工具返回结果**,禁止以纯文本、Markdown 表格或 JSON 代码块等形式直接输出资产列表。
|
||||
`resultTool` 的 schema 会对字段类型和枚举值做强校验,调用时请严格按照下方字段定义填写,确保数据结构正确、字段完整、类型匹配。
|
||||
|
||||
每个资产对象包含以下字段:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ---- | ---- | ---- |
|
||||
| `name` | string | 是 | 资产名称,使用剧本中的原始称呼,不做其他多余描述 |
|
||||
| `desc` | string | 是 | 资产描述,30-80 字的视觉化描述 |
|
||||
| `prompt` | string | 是 | 生成提示词,英文,用于 AI 图片生成 |
|
||||
| `type` | enum | 是 | 资产类型:`role` / `scene` / `tool` / `clip` |
|
||||
|
||||
## 提取规则
|
||||
|
||||
### 角色(role)
|
||||
|
||||
- 提取剧本中出现的所有有名字的角色
|
||||
- `desc`:包含外貌特征、服饰风格、体态气质等视觉要素
|
||||
- `prompt`:英文提示词,描述角色的外观特征,适用于 AI 角色图生成
|
||||
- 同一角色有多个称呼时,取最常用的作为 `name`
|
||||
- 无名龙套(如"路人甲"、"士兵")可跳过,除非其造型对剧情有重要视觉意义
|
||||
|
||||
### 场景(scene)
|
||||
|
||||
- 提取剧本中出现的所有场景/地点
|
||||
- `desc`:包含空间结构、光照氛围、关键陈设、色调基调等视觉要素
|
||||
- `prompt`:英文提示词,描述场景的整体视觉风格,适用于 AI 场景图生成
|
||||
- 同一场景的不同状态(如白天/夜晚)不重复提取,在 `desc` 中注明即可
|
||||
|
||||
### 道具(tool)
|
||||
|
||||
- 提取剧本中出现的重要道具/物品
|
||||
- `desc`:包含外观形状、颜色材质、尺寸参考、特殊效果等视觉要素
|
||||
- `prompt`:英文提示词,描述道具的外观细节,适用于 AI 道具图生成
|
||||
- 仅提取有独立视觉意义或剧情功能的道具,通用物品可跳过
|
||||
|
||||
### 素材片段(clip)
|
||||
|
||||
- 提取剧本中需要的特殊素材片段(如特效、转场、特殊画面等)
|
||||
- `desc`:描述素材的视觉效果和用途
|
||||
- `prompt`:英文提示词,描述素材的视觉特征
|
||||
|
||||
## 提示词(prompt)生成规范
|
||||
|
||||
- 采用逗号分隔的关键词/短语格式
|
||||
- 优先描述**视觉特征**,避免抽象概念
|
||||
- 包含风格关键词(如 anime style, manga style 等,根据项目风格决定)
|
||||
- 角色 prompt 示例:`a young man, sharp eyebrows, black hair, pale skin, wearing a gray Taoist robe, slender build, cold expression`
|
||||
- 场景 prompt 示例:`dark cave interior, glowing crystals on walls, misty atmosphere, dim blue lighting, stone altar in center`
|
||||
- 道具 prompt 示例:`ancient jade pendant, oval shape, translucent green, carved dragon pattern, glowing faintly`
|
||||
|
||||
## 提取流程
|
||||
|
||||
1. 通读剧本全文,识别所有出现的角色、场景、道具
|
||||
2. 对每个资产生成结构化的 `name`、`desc`、`prompt`、`type`
|
||||
3. 去重:同一资产不重复提取
|
||||
4. **必须通过调用 `resultTool` 工具输出完整资产列表**,不要分多次调用,一次性将所有资产放入 `assetsList` 数组中提交
|
||||
|
||||
## 提取原则
|
||||
|
||||
1. **忠于剧本**:所有提取基于剧本中的实际内容,不臆造未出现的资产
|
||||
2. **视觉优先**:描述和提示词聚焦视觉特征,便于 AI 图片生成
|
||||
3. **精简实用**:只提取对制作有实际意义的资产,避免过度提取
|
||||
4. **分类准确**:严格按照 role/scene/tool/clip 分类,不混淆
|
||||
5. **提示词质量**:英文提示词应具体、可执行,能直接用于 AI 图片生成
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 资产列表中**不要包含剧本内容本身**,仅提取所使用到的资产
|
||||
- 角色的随身物品如果有独立剧情功能,应单独作为道具提取
|
||||
- 场景中的固定陈设不需要单独提取为道具,除非该物件有独立剧情作用
|
||||
@ -44,6 +44,13 @@ description: 通用文本分析与内容提取 Agent,支持小说事件提取
|
||||
- **资产类型**:`tool`(对应 `o_assets.type = "tool"`)
|
||||
- **输出**:结构化道具资产表(道具名称、类别、外观描述、尺寸参考、材质质感、功能/用途、首次出场、关联角色、状态变体)+ 高频道具排名
|
||||
|
||||
### 6. 剧本资产提取(script_assets_extract)
|
||||
|
||||
- **触发条件**:用户提供剧本内容,要求从剧本中提取所使用的资产(角色、场景、道具、素材片段)
|
||||
- **参考文件**:`references/script_assets_extract.md`
|
||||
- **资产类型**:`role`、`scene`、`tool`、`clip`(对应 `o_assets.type`)
|
||||
- **输出**:结构化资产列表(资产名称、资产描述、生成提示词、资产类型),通过 `resultTool` 工具调用返回
|
||||
|
||||
## 资产提取分工说明
|
||||
|
||||
当用户要求从小说中提取"所有资产"或"角色场景道具"时,三个资产提取技能应按以下分工协作:
|
||||
|
||||
@ -123,6 +123,7 @@ function runSubAgent(parentCtx: AgentContext) {
|
||||
prompt: z.string().max(100).describe("交给子Agent的任务简约描述"),
|
||||
}),
|
||||
execute: async ({ agent, prompt }) => {
|
||||
//todo 传入md有问题
|
||||
const fn = [executionAI, supervisionAI][subAgentList.indexOf(agent)];
|
||||
//运行子Agent
|
||||
const subTextStream = await fn({ ...parentCtx, text: prompt });
|
||||
|
||||
@ -46,7 +46,8 @@ export async function decisionAI(ctx: AgentContext) {
|
||||
const systemPrompt = buildSystemPrompt(skill.prompt, mem);
|
||||
|
||||
const projectData = await u.db("o_project").where("id", resTool.data.projectId).first();
|
||||
const novelData = await u.db("o_novel").select("id", "chapterIndex as index");
|
||||
const novelData = await u.db("o_novel").where("projectId", resTool.data.projectId).select("id", "chapterIndex as index");
|
||||
console.log("%c Line:50 🥒 novelData", "background:#2eafb0", novelData);
|
||||
|
||||
const projectInfo = [
|
||||
"## 项目信息",
|
||||
@ -57,7 +58,7 @@ export async function decisionAI(ctx: AgentContext) {
|
||||
`目标改编视频画幅:${projectData?.videoRatio ?? "16:9"}`,
|
||||
].join("\n");
|
||||
|
||||
const prefixSystem = `${projectInfo}\n\n## 章节ID映射表\n${novelData.map((i: any) => `- ${i.id}: 第${i.index}章`).join("\n")}\n\n`;
|
||||
const prefixSystem = `${projectInfo}\n\n## 章节ID映射表\n${novelData.map((i: any) => `- 章节ID:${i.id}: 第${i.index}章`).join("\n")}\n\n`;
|
||||
|
||||
const { textStream } = await u.Ai.Text("scriptAgent").stream({
|
||||
system: prefixSystem + systemPrompt,
|
||||
@ -70,6 +71,7 @@ export async function decisionAI(ctx: AgentContext) {
|
||||
...useTools(ctx.resTool),
|
||||
},
|
||||
onFinish: async (completion) => {
|
||||
console.log("%c Line:73 🍧 completion", "background:#93c0a4", completion);
|
||||
await memory.add("assistant:decision", completion.text);
|
||||
},
|
||||
});
|
||||
@ -99,6 +101,7 @@ export async function executionAI(ctx: AgentContext) {
|
||||
...useTools(ctx.resTool),
|
||||
},
|
||||
onFinish: async (completion) => {
|
||||
console.log("%c Line:102 🍻 completion", "background:#fca650", completion);
|
||||
await memory.add("assistant:execution", completion.text);
|
||||
},
|
||||
});
|
||||
@ -125,6 +128,7 @@ export async function supervisionAI(ctx: AgentContext) {
|
||||
...useTools(ctx.resTool),
|
||||
},
|
||||
onFinish: async (completion) => {
|
||||
console.log("%c Line:129 🍣 completion", "background:#3f7cff", completion);
|
||||
await memory.add("assistant:supervision", completion.text);
|
||||
},
|
||||
});
|
||||
|
||||
@ -12,7 +12,7 @@ export const AssetSchema = z.object({
|
||||
type: z.enum(["role", "tool", "scene", "clip"]).describe("资产类型"),
|
||||
});
|
||||
export const ScriptSchema = z.object({
|
||||
id: z.number().describe("剧本ID,如果新增则为空").optional(),
|
||||
id: z.number().describe("剧本ID"),
|
||||
name: z.string().describe("剧本名称"),
|
||||
content: z.string().describe("剧本内容"),
|
||||
});
|
||||
@ -35,13 +35,14 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
|
||||
get_novel_events: tool({
|
||||
description: "获取章节事件",
|
||||
inputSchema: z.object({
|
||||
ids: z.array(z.number()).describe("章节id"),
|
||||
ids: z.array(z.number()).describe("章节id,注意区分"),
|
||||
}),
|
||||
execute: async ({ ids }) => {
|
||||
resTool.systemMessage(`正在阅读 章节事件 数据...`);
|
||||
console.log("[tools] get_novel_events", ids);
|
||||
const data = await u
|
||||
.db("o_novel")
|
||||
.where("projectId", resTool.data.projectId)
|
||||
.select("id", "chapterIndex as index", "reel", "chapter", "chapterData", "event", "eventState")
|
||||
.whereIn("id", ids);
|
||||
const eventString = data.map((i: any) => [`第${i.index}章,标题:${i.chapter},事件:${i.event}`].join("\n")).join("\n");
|
||||
@ -90,41 +91,48 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
insert_script_to_sqlite: tool({
|
||||
description: "将剧本内容插入sqlite数据库,供后续业务使用",
|
||||
update_script_to_sqlite: tool({
|
||||
description: "更新剧本,修改数据库对应剧本,供后续业务使用",
|
||||
inputSchema: z.object({
|
||||
script: ScriptSchema,
|
||||
// assetsList: z.array(AssetSchema).describe("剧本所使用资产列表,注意不要包含剧本内容,仅为所使用到的 道具、人物、场景、素材"),
|
||||
}),
|
||||
execute: async ({ script }) => {
|
||||
console.log("%c Line:103 🍷 script", "background:#42b983", script);
|
||||
// console.log("[tools] insert_script_to_sqlite", assetsList);
|
||||
await u.db("o_script").where({ id: script.id }).update({
|
||||
name: script.name,
|
||||
content: script.content,
|
||||
});
|
||||
|
||||
socket.emit("setPlanData", { key: "script", value: script.id });
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
insert_script_to_sqlite: tool({
|
||||
description: "新增剧本,将剧本内容插入sqlite数据库,供后续业务使用",
|
||||
inputSchema: z.object({
|
||||
script: ScriptSchema.omit({ id: true }),
|
||||
}),
|
||||
execute: async ({ script }) => {
|
||||
console.log("%c Line:103 🍷 script", "background:#42b983", script);
|
||||
|
||||
const [scriptId] = await u.db("o_script").insert({
|
||||
name: script.name,
|
||||
content: script.content,
|
||||
projectId: resTool.data.projectId,
|
||||
createTime: Date.now(),
|
||||
});
|
||||
// if (assetsList && assetsList.length) {
|
||||
// const assetId = [];
|
||||
// for (const i of assetsList) {
|
||||
// if (i.id) {
|
||||
// assetId.push(i.id);
|
||||
// continue;
|
||||
// }
|
||||
// const [id] = await u.db("o_assets").insert({
|
||||
// name: i.name,
|
||||
// prompt: i.prompt,
|
||||
// type: i.type,
|
||||
// describe: i.desc,
|
||||
// projectId: resTool.data.projectId,
|
||||
// startTime: Date.now(),
|
||||
// });
|
||||
// assetId.push(id);
|
||||
// }
|
||||
|
||||
// await u.db("o_scriptAssets").insert(assetId.map((i) => ({ scriptId, assetId: i })));
|
||||
// }
|
||||
socket.emit("setPlanData", { key: "script", value: scriptId });
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
delete_script_to_sqlite: tool({
|
||||
description: "删除剧本,将剧本内容从sqlite数据库中删除",
|
||||
inputSchema: z.object({
|
||||
scriptId: z.string().describe("剧本id"),
|
||||
}),
|
||||
execute: async ({ scriptId }) => {
|
||||
console.log("[tools] delete_script_to_sqlite", scriptId);
|
||||
await u.db("o_script").where({ id: scriptId }).delete();
|
||||
socket.emit("setPlanData", { key: "script", value: scriptId });
|
||||
return true;
|
||||
},
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// @routes-hash 8dc4df496e2771e0081f32f4d8b2c2ae
|
||||
// @routes-hash 557dfd43a824a4bd4170d0e2c9a6b45c
|
||||
import { Express } from "express";
|
||||
|
||||
import route1 from "./routes/agents/clearMemory";
|
||||
@ -71,30 +71,30 @@ import route67 from "./routes/script/getScrptApi";
|
||||
import route68 from "./routes/script/updateScript";
|
||||
import route69 from "./routes/scriptAgent/getPlanData";
|
||||
import route70 from "./routes/scriptAgent/setPlanData";
|
||||
import route71 from "./routes/setting/about/checkUpdate";
|
||||
import route72 from "./routes/setting/agentDeploy/agentSetKey";
|
||||
import route73 from "./routes/setting/agentDeploy/deployAgentModel";
|
||||
import route74 from "./routes/setting/agentDeploy/getAgentDeploy";
|
||||
import route75 from "./routes/setting/dbConfig/clearData";
|
||||
import route76 from "./routes/setting/fileManagement/openFolder";
|
||||
import route77 from "./routes/setting/getTextModel";
|
||||
import route78 from "./routes/setting/loginConfig/getUser";
|
||||
import route79 from "./routes/setting/loginConfig/updateUserPwd";
|
||||
import route80 from "./routes/setting/memoryConfig/delAllMemory";
|
||||
import route81 from "./routes/setting/memoryConfig/getMemory";
|
||||
import route82 from "./routes/setting/memoryConfig/sureMemory";
|
||||
import route83 from "./routes/setting/skillManagement/addSkill";
|
||||
import route84 from "./routes/setting/skillManagement/deleteSkill";
|
||||
import route85 from "./routes/setting/skillManagement/embeddingSkill";
|
||||
import route86 from "./routes/setting/skillManagement/generateDescription";
|
||||
import route87 from "./routes/setting/skillManagement/getSkillList";
|
||||
import route88 from "./routes/setting/skillManagement/scanSkills";
|
||||
import route89 from "./routes/setting/skillManagement/updateSkill";
|
||||
import route90 from "./routes/setting/vendorConfig/addVendor";
|
||||
import route91 from "./routes/setting/vendorConfig/deleteVendor";
|
||||
import route92 from "./routes/setting/vendorConfig/getVendorList";
|
||||
import route93 from "./routes/setting/vendorConfig/modelTest";
|
||||
import route94 from "./routes/setting/vendorConfig/updateVendor";
|
||||
import route71 from "./routes/setting/agentDeploy/agentSetKey";
|
||||
import route72 from "./routes/setting/agentDeploy/deployAgentModel";
|
||||
import route73 from "./routes/setting/agentDeploy/getAgentDeploy";
|
||||
import route74 from "./routes/setting/dbConfig/clearData";
|
||||
import route75 from "./routes/setting/fileManagement/openFolder";
|
||||
import route76 from "./routes/setting/getTextModel";
|
||||
import route77 from "./routes/setting/loginConfig/getUser";
|
||||
import route78 from "./routes/setting/loginConfig/updateUserPwd";
|
||||
import route79 from "./routes/setting/memoryConfig/delAllMemory";
|
||||
import route80 from "./routes/setting/memoryConfig/getMemory";
|
||||
import route81 from "./routes/setting/memoryConfig/sureMemory";
|
||||
import route82 from "./routes/setting/skillManagement/addSkill";
|
||||
import route83 from "./routes/setting/skillManagement/deleteSkill";
|
||||
import route84 from "./routes/setting/skillManagement/embeddingSkill";
|
||||
import route85 from "./routes/setting/skillManagement/generateDescription";
|
||||
import route86 from "./routes/setting/skillManagement/getSkillList";
|
||||
import route87 from "./routes/setting/skillManagement/scanSkills";
|
||||
import route88 from "./routes/setting/skillManagement/updateSkill";
|
||||
import route89 from "./routes/setting/vendorConfig/addVendor";
|
||||
import route90 from "./routes/setting/vendorConfig/deleteVendor";
|
||||
import route91 from "./routes/setting/vendorConfig/getVendorList";
|
||||
import route92 from "./routes/setting/vendorConfig/modelTest";
|
||||
import route93 from "./routes/setting/vendorConfig/updateVendor";
|
||||
import route94 from "./routes/task/getProject";
|
||||
import route95 from "./routes/task/getTaskApi";
|
||||
import route96 from "./routes/task/getTaskCategories";
|
||||
import route97 from "./routes/task/taskDetails";
|
||||
@ -171,30 +171,30 @@ export default async (app: Express) => {
|
||||
app.use("/api/script/updateScript", route68);
|
||||
app.use("/api/scriptAgent/getPlanData", route69);
|
||||
app.use("/api/scriptAgent/setPlanData", route70);
|
||||
app.use("/api/setting/about/checkUpdate", route71);
|
||||
app.use("/api/setting/agentDeploy/agentSetKey", route72);
|
||||
app.use("/api/setting/agentDeploy/deployAgentModel", route73);
|
||||
app.use("/api/setting/agentDeploy/getAgentDeploy", route74);
|
||||
app.use("/api/setting/dbConfig/clearData", route75);
|
||||
app.use("/api/setting/fileManagement/openFolder", route76);
|
||||
app.use("/api/setting/getTextModel", route77);
|
||||
app.use("/api/setting/loginConfig/getUser", route78);
|
||||
app.use("/api/setting/loginConfig/updateUserPwd", route79);
|
||||
app.use("/api/setting/memoryConfig/delAllMemory", route80);
|
||||
app.use("/api/setting/memoryConfig/getMemory", route81);
|
||||
app.use("/api/setting/memoryConfig/sureMemory", route82);
|
||||
app.use("/api/setting/skillManagement/addSkill", route83);
|
||||
app.use("/api/setting/skillManagement/deleteSkill", route84);
|
||||
app.use("/api/setting/skillManagement/embeddingSkill", route85);
|
||||
app.use("/api/setting/skillManagement/generateDescription", route86);
|
||||
app.use("/api/setting/skillManagement/getSkillList", route87);
|
||||
app.use("/api/setting/skillManagement/scanSkills", route88);
|
||||
app.use("/api/setting/skillManagement/updateSkill", route89);
|
||||
app.use("/api/setting/vendorConfig/addVendor", route90);
|
||||
app.use("/api/setting/vendorConfig/deleteVendor", route91);
|
||||
app.use("/api/setting/vendorConfig/getVendorList", route92);
|
||||
app.use("/api/setting/vendorConfig/modelTest", route93);
|
||||
app.use("/api/setting/vendorConfig/updateVendor", route94);
|
||||
app.use("/api/setting/agentDeploy/agentSetKey", route71);
|
||||
app.use("/api/setting/agentDeploy/deployAgentModel", route72);
|
||||
app.use("/api/setting/agentDeploy/getAgentDeploy", route73);
|
||||
app.use("/api/setting/dbConfig/clearData", route74);
|
||||
app.use("/api/setting/fileManagement/openFolder", route75);
|
||||
app.use("/api/setting/getTextModel", route76);
|
||||
app.use("/api/setting/loginConfig/getUser", route77);
|
||||
app.use("/api/setting/loginConfig/updateUserPwd", route78);
|
||||
app.use("/api/setting/memoryConfig/delAllMemory", route79);
|
||||
app.use("/api/setting/memoryConfig/getMemory", route80);
|
||||
app.use("/api/setting/memoryConfig/sureMemory", route81);
|
||||
app.use("/api/setting/skillManagement/addSkill", route82);
|
||||
app.use("/api/setting/skillManagement/deleteSkill", route83);
|
||||
app.use("/api/setting/skillManagement/embeddingSkill", route84);
|
||||
app.use("/api/setting/skillManagement/generateDescription", route85);
|
||||
app.use("/api/setting/skillManagement/getSkillList", route86);
|
||||
app.use("/api/setting/skillManagement/scanSkills", route87);
|
||||
app.use("/api/setting/skillManagement/updateSkill", route88);
|
||||
app.use("/api/setting/vendorConfig/addVendor", route89);
|
||||
app.use("/api/setting/vendorConfig/deleteVendor", route90);
|
||||
app.use("/api/setting/vendorConfig/getVendorList", route91);
|
||||
app.use("/api/setting/vendorConfig/modelTest", route92);
|
||||
app.use("/api/setting/vendorConfig/updateVendor", route93);
|
||||
app.use("/api/task/getProject", route94);
|
||||
app.use("/api/task/getTaskApi", route95);
|
||||
app.use("/api/task/getTaskCategories", route96);
|
||||
app.use("/api/task/taskDetails", route97);
|
||||
|
||||
@ -14,7 +14,7 @@ export default router.post(
|
||||
async (req, res) => {
|
||||
const { assetsId } = req.body;
|
||||
|
||||
const assets = await u.db("o_assets").where("id", assetsId).select("id", "imageId", "type", "state").first();
|
||||
const assets = await u.db("o_assets").where("id", assetsId).select("id", "imageId", "type").first();
|
||||
|
||||
const rawTempAssets = await u.db("o_image").where("assetsId", assetsId).select("id", "filePath", "assetsId", "type", "state");
|
||||
|
||||
@ -28,10 +28,10 @@ export default router.post(
|
||||
|
||||
const data = {
|
||||
id: assets!.id,
|
||||
state: assets!.state,
|
||||
imageId: assets!.imageId ?? null,
|
||||
tempAssets,
|
||||
};
|
||||
console.log("%c Line:30 🥤 data", "background:#465975", data);
|
||||
res.status(200).send(success(data));
|
||||
},
|
||||
);
|
||||
|
||||
@ -122,6 +122,7 @@ export default router.post(
|
||||
messages: [{ role: "user", content: "小说原文" + novelText }],
|
||||
tools: skill.tools,
|
||||
})) as any;
|
||||
|
||||
if (!_output) return res.status(500).send("失败");
|
||||
await u.db("o_assets").where("id", assetsId).update({ prompt: _output });
|
||||
|
||||
|
||||
@ -14,8 +14,13 @@ export default router.post(
|
||||
data: flowDataSchema,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, episodesId } = req.body;
|
||||
const { data, projectId, episodesId } = req.body;
|
||||
const sqlData = await u.db("o_agentWorkData").where("projectId", String(projectId)).andWhere("episodesId", String(episodesId)).first();
|
||||
for (let item of data.storyboard) {
|
||||
await u.db("o_storyboard").where("id", item.id).update({
|
||||
index: item.id,
|
||||
});
|
||||
}
|
||||
if (!sqlData) {
|
||||
await u.db("o_agentWorkData").insert({
|
||||
projectId,
|
||||
|
||||
@ -7,13 +7,13 @@ const router = express.Router();
|
||||
|
||||
// 删除剧本
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
id: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id } = req.body;
|
||||
await u.db("o_script").where({ id }).delete();
|
||||
res.status(200).send(success({ message: "删除剧本成功" }));
|
||||
},
|
||||
"/",
|
||||
validateFields({
|
||||
id: z.array(z.number()),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { id } = req.body;
|
||||
await u.db("o_script").whereIn("id", id).delete();
|
||||
res.status(200).send(success({ message: "删除剧本成功" }));
|
||||
},
|
||||
);
|
||||
|
||||
@ -5,30 +5,88 @@ import { error, success } from "@/lib/responseFormat";
|
||||
import compressing from "compressing";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { useSkill } from "@/utils/agent/skillsTools";
|
||||
import { Output, tool } from "ai";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
export const AssetSchema = z.object({
|
||||
prompt: z.string().describe("生成提示词"),
|
||||
name: z.string().describe("资产名称,仅为名称不做其他任何表述"),
|
||||
desc: z.string().describe("资产描述"),
|
||||
type: z.enum(["role", "tool", "scene"]).describe("资产类型"),
|
||||
});
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
scriptIds: z.array(z.number()),
|
||||
projectId: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { scriptIds } = req.body;
|
||||
const { scriptIds, projectId } = req.body;
|
||||
if (!scriptIds.length) return res.status(400).send(error("请先选择剧本"));
|
||||
const scripts = await u.db("o_script").whereIn("id", scriptIds);
|
||||
const intansce = u.Ai.Text("universalAgent");
|
||||
const skill = await useSkill("universal_agent.md");
|
||||
const novelData = await u.db("o_novel").where("projectId", projectId).select("chapterData");
|
||||
if (!novelData || novelData.length === 0) return res.status(400).send(error("请先上传小说"));
|
||||
|
||||
const resData = await intansce.invoke({
|
||||
system: skill.prompt,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "请根据以下小说章节生成事件摘要:\n",
|
||||
async function getAssets() {
|
||||
return await u.db("o_assets").where("projectId", projectId).select("id", "name");
|
||||
}
|
||||
for (const scriptId of scriptIds) {
|
||||
const resultTool = tool({
|
||||
description: "返回结果时必须调用这个工具,",
|
||||
inputSchema: z.object({
|
||||
assetsList: z.array(AssetSchema).describe("剧本所使用资产列表,注意不要包含剧本内容,仅为所使用到的 道具、人物、场景、素材"),
|
||||
}),
|
||||
execute: async ({ assetsList }) => {
|
||||
console.log("[tools] set_flowData script", assetsList);
|
||||
if (assetsList && assetsList.length) {
|
||||
const assetId = [];
|
||||
const existingAssets = await getAssets();
|
||||
for (const i of assetsList) {
|
||||
if (existingAssets.length) {
|
||||
const exist = existingAssets.find((j) => j.name === i.name);
|
||||
if (exist) {
|
||||
assetId.push(exist.id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const [id] = await u.db("o_assets").insert({
|
||||
name: i.name,
|
||||
prompt: i.prompt,
|
||||
type: i.type,
|
||||
describe: i.desc,
|
||||
projectId: projectId,
|
||||
startTime: Date.now(),
|
||||
});
|
||||
assetId.push(id);
|
||||
}
|
||||
|
||||
await u.db("o_scriptAssets").insert(assetId.map((i) => ({ scriptId: scriptId, assetId: i })));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
],
|
||||
tools: skill.tools,
|
||||
});
|
||||
});
|
||||
try {
|
||||
const skill = await useSkill("universal_agent.md");
|
||||
const resData = await intansce.invoke({
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
skill.prompt +
|
||||
"\n\n提取剧本中涉及的资产(角色、场景、道具),参考技能 script_assets_extract 规范,结果必须通过 resultTool 工具返回。",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `请根据以下剧本提取对应的剧本资产(角色、场景、道具、素材片段):\n\n${scripts.map((i) => i.content).join("\n\n---\n\n")}`,
|
||||
},
|
||||
],
|
||||
tools: { ...skill.tools, resultTool },
|
||||
});
|
||||
console.log("%c Line:47 🥝 resData", "background:#2eafb0", resData);
|
||||
} catch (e) {
|
||||
console.log("%c Line:52 🍢 e", "background:#42b983", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -13,11 +13,11 @@ export default router.post(
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { projectId, agentType } = req.body;
|
||||
const data = await u.db("o_agentWorkData").where({ id: projectId, key: agentType }).first();
|
||||
const data = await u.db("o_agentWorkData").where({ projectId: projectId, key: agentType }).first();
|
||||
|
||||
if (!data) {
|
||||
await u.db("o_agentWorkData").insert({
|
||||
id: projectId,
|
||||
projectId: projectId,
|
||||
key: agentType,
|
||||
data: JSON.stringify({
|
||||
storySkeleton: "",
|
||||
|
||||
@ -19,7 +19,7 @@ export default router.post(
|
||||
const { projectId, agentType, data } = req.body;
|
||||
await u
|
||||
.db("o_agentWorkData")
|
||||
.where({ id: projectId, key: agentType })
|
||||
.where({ projectId: projectId, key: agentType })
|
||||
.update({
|
||||
data: JSON.stringify(data),
|
||||
});
|
||||
|
||||
10
src/routes/task/getProject.ts
Normal file
10
src/routes/task/getProject.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
const router = express.Router();
|
||||
|
||||
export default router.post("/", async (req, res) => {
|
||||
const list = await u.db("o_project").select("id", "name").groupBy("name");
|
||||
const data = list.filter((item) => item.name);
|
||||
res.status(200).send(success(data));
|
||||
});
|
||||
@ -9,11 +9,12 @@ export default router.post(
|
||||
validateFields({
|
||||
state: z.string().optional().nullable(),
|
||||
taskClass: z.string().optional().nullable(),
|
||||
projectId: z.number().optional().nullable(),
|
||||
page: z.number(),
|
||||
limit: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { taskClass, state, page = 1, limit = 10 }: any = req.body;
|
||||
const { taskClass, state, projectId, page = 1, limit = 10 }: any = req.body;
|
||||
const offset = (page - 1) * limit;
|
||||
const data = await u
|
||||
.db("o_tasks")
|
||||
@ -25,6 +26,9 @@ export default router.post(
|
||||
if (state) {
|
||||
qb.andWhere("o_tasks.state", state);
|
||||
}
|
||||
if (projectId) {
|
||||
qb.andWhere("o_tasks.projectId", projectId);
|
||||
}
|
||||
})
|
||||
.select("o_tasks.*", "o_project.* ")
|
||||
.offset(offset)
|
||||
@ -36,6 +40,9 @@ export default router.post(
|
||||
if (taskClass) {
|
||||
qb.andWhere("o_tasks.taskClass", taskClass);
|
||||
}
|
||||
if (projectId) {
|
||||
qb.andWhere("o_tasks.projectId", projectId);
|
||||
}
|
||||
if (state) {
|
||||
qb.andWhere("o_tasks.state", state);
|
||||
}
|
||||
|
||||
@ -1,17 +1,10 @@
|
||||
import express from "express";
|
||||
import u from "@/utils";
|
||||
import { success } from "@/lib/responseFormat";
|
||||
import { validateFields } from "@/middleware/middleware";
|
||||
import { number, z } from "zod";
|
||||
const router = express.Router();
|
||||
|
||||
export default router.post(
|
||||
"/",
|
||||
validateFields({
|
||||
projectId: z.number(),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const data = await u.db("o_tasks").where("projectId", req.body.projectId).select("taskClass").groupBy("taskClass");
|
||||
res.status(200).send(success(data));
|
||||
},
|
||||
);
|
||||
export default router.post("/", async (req, res) => {
|
||||
const list = await u.db("o_tasks").select("taskClass").groupBy("taskClass");
|
||||
const data = list.filter((item) => item.taskClass);
|
||||
res.status(200).send(success(data));
|
||||
});
|
||||
|
||||
34
src/types/database.d.ts
vendored
34
src/types/database.d.ts
vendored
@ -1,6 +1,36 @@
|
||||
// @db-hash d807205fbb27fc5ddb04cae060fb4430
|
||||
// @db-hash 579a004cc745580469a24ee71f5f51c3
|
||||
//该文件由脚本自动生成,请勿手动修改
|
||||
|
||||
export interface _o_project_old_20260326 {
|
||||
'artStyle'?: string | null;
|
||||
'createTime'?: number | null;
|
||||
'id'?: number | null;
|
||||
'intro'?: string | null;
|
||||
'name'?: string | null;
|
||||
'projectType'?: string | null;
|
||||
'type'?: string | null;
|
||||
'userId'?: number | null;
|
||||
'videoRatio'?: string | null;
|
||||
}
|
||||
export interface _o_storyboard_old_20260325 {
|
||||
'camera'?: string | null;
|
||||
'createTime'?: number | null;
|
||||
'description'?: string | null;
|
||||
'duration'?: string | null;
|
||||
'filePath'?: string | null;
|
||||
'frameMode'?: string | null;
|
||||
'id'?: number;
|
||||
'lines'?: string | null;
|
||||
'mode'?: string | null;
|
||||
'model'?: string | null;
|
||||
'prompt'?: string | null;
|
||||
'reason'?: string | null;
|
||||
'resolution'?: string | null;
|
||||
'scriptId'?: number | null;
|
||||
'sound'?: string | null;
|
||||
'state'?: string | null;
|
||||
'title'?: string | null;
|
||||
}
|
||||
export interface memories {
|
||||
'content': string;
|
||||
'createTime': number;
|
||||
@ -220,6 +250,8 @@ export interface o_videoConfig {
|
||||
}
|
||||
|
||||
export interface DB {
|
||||
"_o_project_old_20260326": _o_project_old_20260326;
|
||||
"_o_storyboard_old_20260325": _o_storyboard_old_20260325;
|
||||
"memories": memories;
|
||||
"o_agentDeploy": o_agentDeploy;
|
||||
"o_agentWorkData": o_agentWorkData;
|
||||
|
||||
@ -137,8 +137,6 @@ class AiVideo {
|
||||
async run(input: VideoConfig) {
|
||||
return withTaskRecord(this.key, input.taskClass, input.describe, input.relatedObjects, input.projectId, async (modelName) => {
|
||||
const fn = await getVendorTemplateFn("videoRequest", modelName);
|
||||
|
||||
console.log("%c Line:142 🎂 input", "background:#42b983", input);
|
||||
this.result = await fn(input);
|
||||
if (this.result.startsWith("http")) this.result = await urlToBase64(this.result);
|
||||
return this;
|
||||
|
||||
@ -16,46 +16,68 @@ export interface EventType {
|
||||
|
||||
class CleanNovel {
|
||||
emitter: EventEmitter;
|
||||
constructor() {
|
||||
/** 最大并发数 */
|
||||
concurrency: number;
|
||||
|
||||
constructor(concurrency: number = 5) {
|
||||
this.emitter = new EventEmitter();
|
||||
this.concurrency = concurrency;
|
||||
}
|
||||
|
||||
private async processChapter(novel: o_novel, intansce: ReturnType<typeof u.Ai.Text>): Promise<EventType | null> {
|
||||
try {
|
||||
const skill = await useSkill("universal_agent.md");
|
||||
|
||||
const resData = await intansce.invoke({
|
||||
system: skill.prompt,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "请根据以下小说章节生成事件摘要:\n" + novel.chapterData!,
|
||||
},
|
||||
],
|
||||
tools: skill.tools,
|
||||
});
|
||||
|
||||
const preData = resData.text;
|
||||
|
||||
this.emitter.emit("item", { id: novel.id, event: preData });
|
||||
return { id: novel.id!, event: preData };
|
||||
} catch (e) {
|
||||
this.emitter.emit("item", { id: novel.id, event: null, errorReason: u.error(e).message });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async start(allChapters: o_novel[], projectId: number): Promise<EventType[]> {
|
||||
//所有事件
|
||||
let totalEvent: EventType[] = [];
|
||||
const totalEvent: EventType[] = [];
|
||||
const intansce = u.Ai.Text("universalAgent");
|
||||
|
||||
try {
|
||||
for (let gi = 0; gi < allChapters.length; gi++) {
|
||||
const novel = allChapters[gi];
|
||||
let resData;
|
||||
try {
|
||||
const skill = await useSkill("universal_agent.md");
|
||||
// 并发控制:通过信号量限制同时执行的任务数
|
||||
let running = 0;
|
||||
let index = 0;
|
||||
const results: Promise<void>[] = [];
|
||||
|
||||
resData = await intansce.invoke({
|
||||
system: skill.prompt,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "请根据以下小说章节生成事件摘要:\n" + novel.chapterData!,
|
||||
},
|
||||
],
|
||||
tools: skill.tools,
|
||||
});
|
||||
console.log("%c Line:35 🍆 resData", "background:#fca650", resData);
|
||||
const runNext = (): Promise<void> => {
|
||||
if (index >= allChapters.length) return Promise.resolve();
|
||||
const novel = allChapters[index++];
|
||||
running++;
|
||||
|
||||
const preData = resData.text;
|
||||
return this.processChapter(novel, intansce).then((result) => {
|
||||
if (result) totalEvent.push(result);
|
||||
running--;
|
||||
return runNext();
|
||||
});
|
||||
};
|
||||
|
||||
// 启动最多 concurrency 个并发任务
|
||||
const workers = Array.from(
|
||||
{ length: Math.min(this.concurrency, allChapters.length) },
|
||||
() => runNext()
|
||||
);
|
||||
|
||||
await Promise.all(workers);
|
||||
|
||||
this.emitter.emit("item", { id: novel.id, event: preData });
|
||||
totalEvent.push({ id: novel.id!, event: preData });
|
||||
} catch (e) {
|
||||
console.log("%c Line:51 🍩 e", "background:#93c0a4", e);
|
||||
this.emitter.emit("item", { id: novel.id, event: null, errorReason: u.error(e).message });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
return totalEvent;
|
||||
}
|
||||
}
|
||||
|
||||
106
src/utils/stripThink.ts
Normal file
106
src/utils/stripThink.ts
Normal file
@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 去除深度思考模型输出的 <think>...</think> 标签及其内容
|
||||
*
|
||||
* 1. stripThink(text) — 用于非流式,直接去除完整文本中的 <think> 块
|
||||
* 2. createThinkStreamFilter() — 用于流式,返回有状态的过滤器,逐 chunk 过滤
|
||||
*/
|
||||
|
||||
/**
|
||||
* 非流式:去除完整文本中的 <think>...</think>
|
||||
*/
|
||||
export function stripThink(text: string): string {
|
||||
return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式:创建一个有状态的 chunk 过滤器
|
||||
*
|
||||
* 用法:
|
||||
* ```ts
|
||||
* const filter = createThinkStreamFilter();
|
||||
* for await (const chunk of textStream) {
|
||||
* const filtered = filter.push(chunk);
|
||||
* if (filtered) msg.send(filtered);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function createThinkStreamFilter() {
|
||||
let insideThink = false;
|
||||
let buffer = "";
|
||||
|
||||
return {
|
||||
/**
|
||||
* 输入一个 chunk,返回过滤后需要输出的文本(可能为空字符串)
|
||||
*/
|
||||
push(chunk: string): string {
|
||||
let output = "";
|
||||
let i = 0;
|
||||
|
||||
while (i < chunk.length) {
|
||||
if (insideThink) {
|
||||
// 正在 <think> 内部,寻找 </think>
|
||||
const closeIdx = chunk.indexOf("</think>", i);
|
||||
if (closeIdx !== -1) {
|
||||
// 找到闭合标签,跳过标签内容
|
||||
insideThink = false;
|
||||
i = closeIdx + "</think>".length;
|
||||
} else {
|
||||
// 整个剩余 chunk 都在 think 内,全部丢弃
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// 不在 <think> 内部
|
||||
const openIdx = chunk.indexOf("<think>", i);
|
||||
if (openIdx !== -1) {
|
||||
// 找到开启标签,输出标签之前的内容
|
||||
output += buffer + chunk.slice(i, openIdx);
|
||||
buffer = "";
|
||||
insideThink = true;
|
||||
i = openIdx + "<think>".length;
|
||||
} else {
|
||||
// 没有发现 <think>,但可能 chunk 末尾是不完整的 "<thi..."
|
||||
// 缓冲末尾可能是 "<" 开头的不完整标签片段
|
||||
const potentialStart = findPartialTag(chunk, i);
|
||||
if (potentialStart !== -1) {
|
||||
output += buffer + chunk.slice(i, potentialStart);
|
||||
buffer = chunk.slice(potentialStart);
|
||||
} else {
|
||||
output += buffer + chunk.slice(i);
|
||||
buffer = "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
},
|
||||
|
||||
/**
|
||||
* 流结束时调用,刷出缓冲区中残留的内容
|
||||
*/
|
||||
flush(): string {
|
||||
const remaining = buffer;
|
||||
buffer = "";
|
||||
return remaining;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 chunk[startIdx..] 的末尾是否包含 "<think>" 的不完整前缀
|
||||
* 如 "<", "<t", "<th", "<thi", "<thin", "<think"
|
||||
* 返回不完整前缀的起始位置,未找到则返回 -1
|
||||
*/
|
||||
function findPartialTag(chunk: string, startIdx: number): number {
|
||||
const tag = "<think>";
|
||||
// 只需检查末尾最多 tag.length - 1 个字符
|
||||
const searchStart = Math.max(startIdx, chunk.length - (tag.length - 1));
|
||||
for (let i = searchStart; i < chunk.length; i++) {
|
||||
const remaining = chunk.slice(i);
|
||||
if (tag.startsWith(remaining)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@ -44,7 +44,6 @@ export default function runCode(code: string) {
|
||||
|
||||
return exports as Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩图片,目标字节数不高于 size
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user