diff --git a/src/agents/productionAgent/index.ts b/src/agents/productionAgent/index.ts index eb9553d..7d2a929 100644 --- a/src/agents/productionAgent/index.ts +++ b/src/agents/productionAgent/index.ts @@ -120,7 +120,7 @@ function runSubAgent(parentCtx: AgentContext) { description: "启动子Agent执行独立任务。可用子Agent:executionAI, decisionAI, supervisionAI", inputSchema: z.object({ agent: z.enum(["executionAI", "supervisionAI"]).describe("子Agent名称"), - prompt: z.string().describe("交给子Agent的任务描述"), + prompt: z.string().max(100).describe("交给子Agent的任务简约描述"), }), execute: async ({ agent, prompt }) => { const fn = [executionAI, supervisionAI][subAgentList.indexOf(agent)]; diff --git a/src/agents/productionAgent/tools.ts b/src/agents/productionAgent/tools.ts index 7cf6d69..17a842a 100644 --- a/src/agents/productionAgent/tools.ts +++ b/src/agents/productionAgent/tools.ts @@ -6,7 +6,7 @@ import u from "@/utils"; import { useSkill } from "@/utils/agent/skillsTools"; import { urlToBase64 } from "@/utils/vm"; export const deriveAssetSchema = z.object({ - id: z.number().describe("衍生资产ID,如果新增则为空").optional(), + id: z.number().describe("衍生资产ID,如果新增则为空"), assetsId: z.number().describe("关联的资产ID"), prompt: z.string().describe("生成提示词"), name: z.string().describe("衍生资产名称"), @@ -24,7 +24,7 @@ export const assetItemSchema = z.object({ derive: z.array(deriveAssetSchema).describe("衍生资产列表"), }); export const storyboardSchema = z.object({ - id: z.number().optional().describe("分镜ID,未从工作区获得的分镜面板视为需要新增;如需新增则为空"), + id: z.number().describe("分镜ID,必须为真实id"), title: z.string().describe("分镜标题"), description: z.string().describe("分镜描述"), camera: z.string().describe("镜头信息"), @@ -104,83 +104,156 @@ export default (resTool: ResTool, toolsNames?: string[]) => { return true; }, }), - // add_flowData_assets: tool({ - // description: "新增对应衍生资产列表到工作区,严禁包含 不需要新增的数据", - // inputSchema: z.object({ value: z.array(deriveAssetSchema).describe("需要新增的资产列表") }), - // execute: async ({ value }) => { - // console.log("[tools] set_flowData add_flowData_assets", value); - // resTool.systemMessage("正在保存 衍生资产 数据"); - // const addAssetsData = []; - // if (value && Array.isArray(value) && value.length) { - // for (const i of value) { - // const [insertedId] = await u.db("o_assets").insert({ - // assetsId: +i.assetsId || null, - // projectId: resTool.data.projectId, - // name: i.name, - // type: i.type, - // prompt: i.prompt, - // describe: i.desc, - // startTime: Date.now(), - // }); - // console.log("%c Line:141 🍑 resTool.data.scriptId", "background:#ea7e5c", resTool.data.scriptId); - // await u.db("o_scriptAssets").insert({ - // scriptId: resTool.data.scriptId, - // assetId: insertedId, - // }); - // addAssetsData.push({ - // ...i, - // id: insertedId, - // }); - // } - // } - // socket.emit("setFlowData", { key: "addAssets", value: addAssetsData }); - // return true; - // }, - // }), - set_flowData_assets: tool({ - description: "保存衍生资产列表到工作区", - inputSchema: z.object({ value: flowDataSchema.shape.assets }), + add_flowData_assets: tool({ + description: "新增对应衍生资产列表到工作区,严禁包含 不需要新增的数据", + inputSchema: z.object({ value: z.array(deriveAssetSchema.omit({ id: true })).describe("需要新增的衍生资产列表") }), execute: async ({ value }) => { - console.log("[tools] set_flowData assets", value); + console.log("[tools] set_flowData add_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; - } - } + const setData = [...value] as z.infer[]; + 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[]> = {}; + setData.forEach((i) => { + if (watiAddAssetsMap[i.assetsId]) { + watiAddAssetsMap[i.assetsId].push(i); + } else { + watiAddAssetsMap[i.assetsId] = [i]; } - } - socket.emit("setFlowData", { key: "assets", value }); + }); + 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]]; + } + }); + 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); + resTool.systemMessage("正在保存 衍生资产 数据"); + 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[]> = {}; + 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); + } + }); + 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); + resTool.systemMessage("正在保存 衍生资产 数据"); + 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)); + }); + // 将 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 }), @@ -191,37 +264,129 @@ export default (resTool: ResTool, toolsNames?: string[]) => { return true; }, }), - set_flowData_storyboard: tool({ - description: "保存分镜面板到工作区", + 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); + resTool.systemMessage("正在新增 分镜面板 数据..."); + const setData = [...value] as z.infer[]; + 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, + }); + if (item.associateAssetsIds.length) { + await u.db("o_assets2Storyboard").insert(item.associateAssetsIds.map((i) => ({ storyboardId: insertedId, assetId: i }))); + } + item.id = insertedId; + } + + 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 }); + return true; + }, + }), + update_flowData_storyboard: tool({ + description: "更新指定分镜面板到工作区", inputSchema: z.object({ value: flowDataSchema.shape.storyboard }), execute: async ({ value }) => { - console.log("[tools] set_flowData storyboard", value); - resTool.systemMessage("正在保存 分镜面板 数据..."); + console.log("[tools] update_flowData storyboard", value); + resTool.systemMessage("正在更新 分镜面板 数据..."); for (const item of value) { - if (!item.id) { - const [insertedId] = await u.db("o_storyboard").insert({ + await u + .db("o_storyboard") + .where("id", item.id) + .update({ 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, }); - 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 }); + 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 }); 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); + resTool.systemMessage("正在删除指定 分镜面板 数据..."); + 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 }); + 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 }), @@ -242,85 +407,173 @@ export default (resTool: ResTool, toolsNames?: string[]) => { return true; }, }), - - //todo referenceIds 图片未使用 提示词待调 + // todo 提示词待调 generate_storyboard_images: tool({ - description: `生成一组图片任务,支持图片间的依赖关系(以图生图)。 + description: `生成一组图片任务,支持图片间的依赖关系(以图生图),基于有向无环图(DAG)拓扑排序执行。 参数说明: - images: 图片任务数组 - - id: 图片唯一标识符 + - id: 图片唯一标识符(分镜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: []} + {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.string()).describe("依赖的参考图id数组,无依赖填空数组[]"), - assetIds: z.array(z.number()).optional().describe("参考的资产图"), + referenceIds: z.array(z.number()).describe("依赖的参考 分镜图id数组,无依赖填空数组[]"), + assetIds: z.array(z.number()).describe("参考的资产图"), }), ), }), execute: async ({ images }) => { - console.log("[tools] generated_assets", images); + console.log("[tools] generate_storyboard_images", images); + + // --- 构建任务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(); + // adjacency: 被依赖者 -> 依赖它的节点列表 + const adjacency = new Map(); - const skill = await useSkill("universal_agent.md"); for (const item of images) { - resTool.systemMessage(`生在生成分镜 id:${item.id} 图片`); - //更新对应分镜状态 - await u.db("o_storyboard").where("id", item.id).update({ state: "生成中" }); - // 异步生成 - const imageModel = resTool.data.imageModel; - - u.Ai.Image(imageModel?.modelId) - .run({ - systemPrompt: skill.prompt, - prompt: item.prompt, - imageBase64: await getAssetsImageBase64(item.assetIds ?? []), - size: imageModel?.quality, - aspectRatio: imageModel?.ratio, - taskClass: "生成图片", - describe: "分镜图片生成", - relatedObjects: "hhhh", - projectId: resTool.data.projectId, - }) - .then(async (imageCls) => { - const savePath = `/${resTool.data.projectId}/storyboard/${u.uuid()}.jpg`; - await imageCls.save(savePath); - const obj = { - ...item, - id: item.id, - src: await u.oss.getFileUrl(savePath), - state: "已完成", - }; - // 更新对应分镜状态 - await u.db("o_storyboard").where("id", item.id).update({ state: "已完成", filePath: savePath }); - // 前端对话框提示 - resTool.systemMessage(`分镜 id:${item.id} 图片生成完成`); - // 更新前端界面展示 - socket.emit("setFlowData", { key: "setStoryboardImage", value: obj }); - }); - //更新前端为生成中 - socket.emit("setFlowData", { key: "setStoryboardImage", value: { ...item, id: item.id, src: "", state: "生成中" } }); + // 只统计本批次内的依赖作为入度 + 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); + } } - return "分镜图片生成中"; + + // 拓扑排序,按层级分组(同层可并行) + const levels: number[][] = []; + let queue = images.filter((item) => (inDegree.get(item.id) ?? 0) === 0).map((item) => item.id); + + const visited = new Set(); + 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); + resTool.systemMessage(`检测到循环依赖,涉及分镜id: ${cyclicIds.join(", ")},请修正后重试`); + return `错误:检测到循环依赖,涉及分镜id: ${cyclicIds.join(", ")}`; + } + + console.log("%c Line:496 🌶", "background:#ea7e5c"); + resTool.systemMessage(`图片生成调度计划:共 ${levels.length} 层,${images.length} 张图片`); + + // --- 准备公共数据 --- + const skill = await useSkill("universal-agent"); + const projectData = await u.db("o_project").where("id", resTool.data.projectId).select("videoRatio").first(); + const imageModel = resTool.data.imageModel; + + // 生成单张图片的函数 + const generateOneImage = async (item: (typeof images)[0]) => { + resTool.systemMessage(`正在生成分镜 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(imageModel?.modelId).run({ + systemPrompt: skill.prompt, + prompt: item.prompt, + imageBase64: [...assetsBase64, ...referenceBase64], + size: imageModel?.quality, + 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, + }; + // 前端对话框提示 + resTool.systemMessage(`分镜 id:${item.id} 图片生成完成`); + // 更新前端界面展示 + 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)!); + resTool.systemMessage(`开始生成第 ${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); + resTool.systemMessage(`分镜 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: "生成失败" }, + }); + } + } + } + + return "分镜图片生成完成"; }, }), - //todo 图片是否需要参考 原资产 提示词待调 + //todo 提示词待调 generate_assets_images: tool({ description: ` 生成 资产图片 不区分原资产于衍生资产 @@ -333,12 +586,34 @@ export default (resTool: ResTool, toolsNames?: string[]) => { {assetId: 1, prompt: "一张猫的图片"} ] `, - inputSchema: z.object({ images: z.array(z.object({ assetId: z.number(), prompt: z.string() })) }), + inputSchema: z.object({ + images: z.array( + z.object({ + assetId: z.number().describe("衍生资产id"), + prompt: z.string().describe("提示词"), + }), + ), + }), execute: async ({ images }) => { const skill = await useSkill("universal_agent.md"); + 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 imageModel = resTool.data.imageModel; - for (const item of images) { + for (const item of assetsImage) { const [imageId] = await u.db("o_image").insert({ // 数据库插入图片记录 assetsId: item.assetId, @@ -348,11 +623,11 @@ export default (resTool: ResTool, toolsNames?: string[]) => { }); u.Ai.Image(imageModel?.modelId) .run({ - systemPrompt: skill.prompt, + // systemPrompt: skill.prompt, prompt: item.prompt, - imageBase64: [], + imageBase64: await getAssetsImageBase64(item.id ? [item.id] : []), size: imageModel?.quality, - aspectRatio: imageModel?.ratio, + aspectRatio: "16:9", taskClass: "生成图片", describe: "资产图片生成", relatedObjects: "hhhh", @@ -376,7 +651,6 @@ export default (resTool: ResTool, toolsNames?: string[]) => { //通知前端更新状态 socket.emit("setFlowData", { key: "setAssetsImage", value: { ...item, id: item.assetId, src: "", state: "生成中" } }); } - console.log("[tools] generate_assets_images", images); return "资产生成中"; }, }), @@ -385,6 +659,7 @@ export default (resTool: ResTool, toolsNames?: string[]) => { 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 @@ -396,7 +671,31 @@ async function getAssetsImageBase64(imageIds: number[]) { const imageUrls = await Promise.all( imagePaths.map(async (i) => { if (i.filePath) { - return await urlToBase64(await u.oss.getFileUrl(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; } diff --git a/src/agents/scriptAgent/index.ts b/src/agents/scriptAgent/index.ts index d94555a..adce6f7 100644 --- a/src/agents/scriptAgent/index.ts +++ b/src/agents/scriptAgent/index.ts @@ -140,7 +140,7 @@ function runSubAgent(parentCtx: AgentContext) { description: "启动子Agent执行独立任务。可用子Agent:executionAI, decisionAI, supervisionAI", inputSchema: z.object({ agent: z.enum(["executionAI", "supervisionAI"]).describe("子Agent名称"), - prompt: z.string().describe("交给子Agent的任务描述"), + prompt: z.string().max(100).describe("交给子Agent的任务简约描述"), }), execute: async ({ agent, prompt }) => { const fn = [executionAI, supervisionAI][subAgentList.indexOf(agent)]; diff --git a/src/agents/scriptAgent/tools.ts b/src/agents/scriptAgent/tools.ts index c945515..b4abe1b 100644 --- a/src/agents/scriptAgent/tools.ts +++ b/src/agents/scriptAgent/tools.ts @@ -108,6 +108,10 @@ export default (resTool: ResTool, toolsNames?: string[]) => { 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, diff --git a/src/lib/initDB.ts b/src/lib/initDB.ts index 179c877..a6fd40d 100644 --- a/src/lib/initDB.ts +++ b/src/lib/initDB.ts @@ -30,6 +30,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => builder: (table) => { table.integer("id"); table.string("projectType"); + table.string("model"); table.text("name"); table.text("intro"); table.text("type"); @@ -310,6 +311,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => table.text("lines"); table.text("state"); table.text("reason"); + table.text("index"); table.integer("createTime"); table.primary(["id"]); table.unique(["id"]); @@ -415,7 +417,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => builder: (table) => { table.integer("storyboardId").notNullable(); table.integer("assetId").notNullable(); - table.primary(["assetId", "assetId"]); + table.primary(["storyboardId", "assetId"]); table.unique(["storyboardId", "assetId"]); }, }, diff --git a/src/router.ts b/src/router.ts index 1eb3676..291c400 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,4 +1,4 @@ -// @routes-hash 62dafbea4285d1f7bd1a6acf5ddbfacc +// @routes-hash f5f78866e59979bf30af031c9ea0de82 import { Express } from "express"; import route1 from "./routes/agents/clearMemory"; @@ -49,49 +49,43 @@ import route45 from "./routes/production/getFlowData"; import route46 from "./routes/production/getProductionData"; import route47 from "./routes/production/getStoryboardData"; import route48 from "./routes/production/saveFlowData"; -import route49 from "./routes/production/workbench/confirmSelection"; -import route50 from "./routes/production/workbench/delVideo"; -import route51 from "./routes/production/workbench/generateVideo"; -import route52 from "./routes/production/workbench/getChatLines"; -import route53 from "./routes/production/workbench/getVideoModelDetail"; -import route54 from "./routes/production/workbench/videoPolling"; -import route55 from "./routes/project/addProject"; -import route56 from "./routes/project/delProject"; -import route57 from "./routes/project/editProject"; -import route58 from "./routes/project/getProject"; -import route59 from "./routes/script/addScript"; -import route60 from "./routes/script/delScript"; -import route61 from "./routes/script/exportScript"; -import route62 from "./routes/script/getScrptApi"; -import route63 from "./routes/script/updateScript"; -import route64 from "./routes/scriptAgent/getPlanData"; -import route65 from "./routes/scriptAgent/setPlanData"; -import route66 from "./routes/setting/agentDeploy/agentSetKey"; -import route67 from "./routes/setting/agentDeploy/deployAgentModel"; -import route68 from "./routes/setting/agentDeploy/getAgentDeploy"; -import route69 from "./routes/setting/dbConfig/clearData"; -import route70 from "./routes/setting/fileManagement/openFolder"; -import route71 from "./routes/setting/getTextModel"; -import route72 from "./routes/setting/loginConfig/getUser"; -import route73 from "./routes/setting/loginConfig/updateUserPwd"; -import route74 from "./routes/setting/memoryConfig/getMemory"; -import route75 from "./routes/setting/memoryConfig/sureMemory"; -import route76 from "./routes/setting/skillManagement/addSkill"; -import route77 from "./routes/setting/skillManagement/deleteSkill"; -import route78 from "./routes/setting/skillManagement/embeddingSkill"; -import route79 from "./routes/setting/skillManagement/generateDescription"; -import route80 from "./routes/setting/skillManagement/getSkillList"; -import route81 from "./routes/setting/skillManagement/scanSkills"; -import route82 from "./routes/setting/skillManagement/updateSkill"; -import route83 from "./routes/setting/vendorConfig/addVendor"; -import route84 from "./routes/setting/vendorConfig/deleteVendor"; -import route85 from "./routes/setting/vendorConfig/getVendorList"; -import route86 from "./routes/setting/vendorConfig/modelTest"; -import route87 from "./routes/setting/vendorConfig/updateVendor"; -import route88 from "./routes/task/getTaskApi"; -import route89 from "./routes/task/getTaskCategories"; -import route90 from "./routes/task/taskDetails"; -import route91 from "./routes/test/test"; +import route49 from "./routes/production/storyboard/previewImage"; +import route50 from "./routes/production/workbench/confirmSelection"; +import route51 from "./routes/production/workbench/delVideo"; +import route52 from "./routes/production/workbench/generateVideo"; +import route53 from "./routes/production/workbench/getChatLines"; +import route54 from "./routes/production/workbench/getVideoModelDetail"; +import route55 from "./routes/production/workbench/videoPolling"; +import route56 from "./routes/project/addProject"; +import route57 from "./routes/project/delProject"; +import route58 from "./routes/project/editProject"; +import route59 from "./routes/project/getProject"; +import route60 from "./routes/script/addScript"; +import route61 from "./routes/script/delScript"; +import route62 from "./routes/script/exportScript"; +import route63 from "./routes/script/getScrptApi"; +import route64 from "./routes/script/updateScript"; +import route65 from "./routes/scriptAgent/getPlanData"; +import route66 from "./routes/scriptAgent/setPlanData"; +import route67 from "./routes/setting/agentDeploy/agentSetKey"; +import route68 from "./routes/setting/agentDeploy/deployAgentModel"; +import route69 from "./routes/setting/agentDeploy/getAgentDeploy"; +import route70 from "./routes/setting/dbConfig/clearData"; +import route71 from "./routes/setting/fileManagement/openFolder"; +import route72 from "./routes/setting/getTextModel"; +import route73 from "./routes/setting/loginConfig/getUser"; +import route74 from "./routes/setting/loginConfig/updateUserPwd"; +import route75 from "./routes/setting/memoryConfig/getMemory"; +import route76 from "./routes/setting/memoryConfig/sureMemory"; +import route77 from "./routes/setting/vendorConfig/addVendor"; +import route78 from "./routes/setting/vendorConfig/deleteVendor"; +import route79 from "./routes/setting/vendorConfig/getVendorList"; +import route80 from "./routes/setting/vendorConfig/modelTest"; +import route81 from "./routes/setting/vendorConfig/updateVendor"; +import route82 from "./routes/task/getTaskApi"; +import route83 from "./routes/task/getTaskCategories"; +import route84 from "./routes/task/taskDetails"; +import route85 from "./routes/test/test"; export default async (app: Express) => { app.use("/api/agents/clearMemory", route1); @@ -142,47 +136,41 @@ export default async (app: Express) => { app.use("/api/production/getProductionData", route46); app.use("/api/production/getStoryboardData", route47); app.use("/api/production/saveFlowData", route48); - app.use("/api/production/workbench/confirmSelection", route49); - app.use("/api/production/workbench/delVideo", route50); - app.use("/api/production/workbench/generateVideo", route51); - app.use("/api/production/workbench/getChatLines", route52); - app.use("/api/production/workbench/getVideoModelDetail", route53); - app.use("/api/production/workbench/videoPolling", route54); - app.use("/api/project/addProject", route55); - app.use("/api/project/delProject", route56); - app.use("/api/project/editProject", route57); - app.use("/api/project/getProject", route58); - app.use("/api/script/addScript", route59); - app.use("/api/script/delScript", route60); - app.use("/api/script/exportScript", route61); - app.use("/api/script/getScrptApi", route62); - app.use("/api/script/updateScript", route63); - app.use("/api/scriptAgent/getPlanData", route64); - app.use("/api/scriptAgent/setPlanData", route65); - app.use("/api/setting/agentDeploy/agentSetKey", route66); - app.use("/api/setting/agentDeploy/deployAgentModel", route67); - app.use("/api/setting/agentDeploy/getAgentDeploy", route68); - app.use("/api/setting/dbConfig/clearData", route69); - app.use("/api/setting/fileManagement/openFolder", route70); - app.use("/api/setting/getTextModel", route71); - app.use("/api/setting/loginConfig/getUser", route72); - app.use("/api/setting/loginConfig/updateUserPwd", route73); - app.use("/api/setting/memoryConfig/getMemory", route74); - app.use("/api/setting/memoryConfig/sureMemory", route75); - app.use("/api/setting/skillManagement/addSkill", route76); - app.use("/api/setting/skillManagement/deleteSkill", route77); - app.use("/api/setting/skillManagement/embeddingSkill", route78); - app.use("/api/setting/skillManagement/generateDescription", route79); - app.use("/api/setting/skillManagement/getSkillList", route80); - app.use("/api/setting/skillManagement/scanSkills", route81); - app.use("/api/setting/skillManagement/updateSkill", route82); - app.use("/api/setting/vendorConfig/addVendor", route83); - app.use("/api/setting/vendorConfig/deleteVendor", route84); - app.use("/api/setting/vendorConfig/getVendorList", route85); - app.use("/api/setting/vendorConfig/modelTest", route86); - app.use("/api/setting/vendorConfig/updateVendor", route87); - app.use("/api/task/getTaskApi", route88); - app.use("/api/task/getTaskCategories", route89); - app.use("/api/task/taskDetails", route90); - app.use("/api/test/test", route91); + app.use("/api/production/storyboard/previewImage", route49); + app.use("/api/production/workbench/confirmSelection", route50); + app.use("/api/production/workbench/delVideo", route51); + app.use("/api/production/workbench/generateVideo", route52); + app.use("/api/production/workbench/getChatLines", route53); + app.use("/api/production/workbench/getVideoModelDetail", route54); + app.use("/api/production/workbench/videoPolling", route55); + app.use("/api/project/addProject", route56); + app.use("/api/project/delProject", route57); + app.use("/api/project/editProject", route58); + app.use("/api/project/getProject", route59); + app.use("/api/script/addScript", route60); + app.use("/api/script/delScript", route61); + app.use("/api/script/exportScript", route62); + app.use("/api/script/getScrptApi", route63); + app.use("/api/script/updateScript", route64); + app.use("/api/scriptAgent/getPlanData", route65); + app.use("/api/scriptAgent/setPlanData", route66); + app.use("/api/setting/agentDeploy/agentSetKey", route67); + app.use("/api/setting/agentDeploy/deployAgentModel", route68); + app.use("/api/setting/agentDeploy/getAgentDeploy", route69); + app.use("/api/setting/dbConfig/clearData", route70); + app.use("/api/setting/fileManagement/openFolder", route71); + app.use("/api/setting/getTextModel", route72); + app.use("/api/setting/loginConfig/getUser", route73); + app.use("/api/setting/loginConfig/updateUserPwd", route74); + app.use("/api/setting/memoryConfig/getMemory", route75); + app.use("/api/setting/memoryConfig/sureMemory", route76); + app.use("/api/setting/vendorConfig/addVendor", route77); + app.use("/api/setting/vendorConfig/deleteVendor", route78); + app.use("/api/setting/vendorConfig/getVendorList", route79); + app.use("/api/setting/vendorConfig/modelTest", route80); + app.use("/api/setting/vendorConfig/updateVendor", route81); + app.use("/api/task/getTaskApi", route82); + app.use("/api/task/getTaskCategories", route83); + app.use("/api/task/taskDetails", route84); + app.use("/api/test/test", route85); } diff --git a/src/routes/production/editImage/generateFlowImage.ts b/src/routes/production/editImage/generateFlowImage.ts index 46f6453..7a0291e 100644 --- a/src/routes/production/editImage/generateFlowImage.ts +++ b/src/routes/production/editImage/generateFlowImage.ts @@ -83,6 +83,7 @@ export default router.post( async (req, res) => { const { model, references = {}, quality, ratio, prompt, projectId, type } = req.body; const { prompt: userPrompt, images: base64Images } = await convertDirectiveAndImages(references, prompt); + console.log("%c Line:86 🥒 base64Images", "background:#42b983", base64Images.map((s) => s.slice(0, 4))); const imageClass = await u.Ai.Image(model).run({ prompt: userPrompt, imageBase64: base64Images, diff --git a/src/routes/production/editImage/saveImageFlow.ts b/src/routes/production/editImage/saveImageFlow.ts index f17cba7..73e959b 100644 --- a/src/routes/production/editImage/saveImageFlow.ts +++ b/src/routes/production/editImage/saveImageFlow.ts @@ -13,9 +13,10 @@ export default router.post( imageUrl: z.string(), id: z.number().nullable().optional(), type: z.enum(["role", "scene", "storyboard", "clip", "tool"]), + episodesId: z.number(), }), async (req, res) => { - const { edges, nodes, imageUrl, id, type } = req.body; + const { edges, nodes, imageUrl, id, type, episodesId } = req.body; let imagePath = ""; try { imagePath = new URL(imageUrl).pathname; @@ -56,6 +57,7 @@ export default router.post( } else { const [storyboardId] = await u.db("o_storyboard").insert({ filePath: imagePath, + scriptId: episodesId, createTime: Date.now(), }); insertFlowId = storyboardId; @@ -66,6 +68,6 @@ export default router.post( flowData: JSON.stringify({ edges, nodes }), ...(type == "assets" ? { assetsId: insertFlowId } : { storyboardId: insertFlowId }), }); - return res.status(200).send(success()); + return res.status(200).send(success({ id: insertFlowId })); }, ); diff --git a/src/routes/production/editImage/updateImageFlow.ts b/src/routes/production/editImage/updateImageFlow.ts index 4c03c56..2953c65 100644 --- a/src/routes/production/editImage/updateImageFlow.ts +++ b/src/routes/production/editImage/updateImageFlow.ts @@ -14,9 +14,10 @@ export default router.post( imageUrl: z.string(), type: z.enum(["role", "scene", "storyboard", "clip", "tool"]), flowId: z.number(), + episodesId: z.number(), }), async (req, res) => { - const { edges, nodes, id, imageUrl, flowId, type } = req.body; + const { edges, nodes, id, imageUrl, flowId, type, episodesId } = req.body; nodes.forEach((node: any) => { if (node.type == "upload") { node.data.image = node.data.image ? new URL(node.data.image).pathname : ""; @@ -30,7 +31,6 @@ export default router.post( imagePath = new URL(imageUrl).pathname; } catch (e) {} if (imagePath) { - console.log("%c Line:34 🍰", "background:#33a5ff"); if (type == "storyboard") { await u.db("o_storyboard").where("id", id).update({ filePath: imagePath, diff --git a/src/routes/production/getFlowData.ts b/src/routes/production/getFlowData.ts index 00f23e3..5a52ee6 100644 --- a/src/routes/production/getFlowData.ts +++ b/src/routes/production/getFlowData.ts @@ -38,7 +38,6 @@ export default router.post( .where("o_assets.projectId", projectId) .where("o_assets.id", "in", assetIds) .whereNotNull("o_assets.assetsId"); - console.log("%c Line:35 🥚 childAssetsData", "background:#f5ce50", childAssetsData); if (!sqlData) { const flowData: FlowData = { @@ -86,34 +85,123 @@ export default router.post( return res.status(200).send(success(flowData)); } else { try { - const flowData = JSON.parse(sqlData!.data ?? "{}"); - flowData.assets = await Promise.all( - assetsData.map(async (item) => ({ - id: item.id, - name: item.name ?? "", - type: item.type ?? "", - prompt: item.prompt ?? "", - desc: item.describe ?? "", - src: item.filePath && (await u.oss.getFileUrl(item.filePath!)), - derive: await Promise.all( - childAssetsData - .filter((child) => child.assetsId === item.id) - .map(async (child) => ({ - id: child.id, - assetsId: item.id, - name: child.name ?? "", - prompt: child.prompt, - type: child.type, - desc: child.describe ?? "", - src: child.filePath && (await u.oss.getFileUrl(child.filePath!)), - state: child.state ?? "未生成", //todo:矫正状态值 - })), - ), - })), + const storyboardData = await u.db("o_storyboard").where("scriptId", episodesId); + console.log("%c Line:90 🍡 storyboardData", "background:#ed9ec7", storyboardData.length); + await Promise.all( + storyboardData.map(async (i) => { + if (i.filePath) { + try { + i.filePath = await u.oss.getFileUrl(i.filePath); + } catch { + i.filePath = ""; + } + } else { + i.filePath = ""; + } + }), ); + const storyboardIds = storyboardData.map((i) => i.id); + const assetsIds = await u.db("o_assets2Storyboard").whereIn("storyboardId", storyboardIds); + const assets2StoryboardMap: Record = {}; + assetsIds.forEach((i) => { + if (!assets2StoryboardMap[i.storyboardId!]) { + assets2StoryboardMap[i.storyboardId!] = []; + } + assets2StoryboardMap[i.storyboardId!].push(i.assetId!); + }); + const flowData = JSON.parse(sqlData!.data ?? "{}"); + // 将原有 flowData.assets 按 id 建立索引,以便后续合并保留旧字段 + const existingAssetsMap: Record = {}; + if (Array.isArray(flowData.assets)) { + flowData.assets.forEach((a: any) => { + existingAssetsMap[a.id] = a; + }); + } + flowData.assets = await Promise.all( + assetsData.map(async (item) => { + const existing = existingAssetsMap[item.id] ?? {}; + // 将原有 derive 按 id 建立索引 + const existingDeriveMap: Record = {}; + if (Array.isArray(existing.derive)) { + existing.derive.forEach((d: any) => { + existingDeriveMap[d.id] = d; + }); + } + return { + ...existing, + id: item.id, + name: item.name ?? "", + type: item.type ?? "", + prompt: item.prompt ?? "", + desc: item.describe ?? "", + src: item.filePath && (await u.oss.getFileUrl(item.filePath!)), + derive: await Promise.all( + childAssetsData + .filter((child) => child.assetsId === item.id) + .map(async (child) => ({ + ...(existingDeriveMap[child.id] ?? {}), + id: child.id, + assetsId: item.id, + name: child.name ?? "", + prompt: child.prompt, + type: child.type, + desc: child.describe ?? "", + src: child.filePath && (await u.oss.getFileUrl(child.filePath!)), + state: child.state ?? "未生成", //todo:矫正状态值 + })), + ), + }; + }), + ); + // 将数据库 storyboardData 按 id 建立索引 + const dbStoryboardMap: Record = {}; + storyboardData.forEach((i) => { + dbStoryboardMap[i.id!] = i; + }); + + // 用于构造单条 storyboard 的辅助函数 + const buildStoryboardItem = (i: (typeof storyboardData)[number], existing: any = {}) => ({ + ...existing, + id: i.id, + title: i.title, + description: i.description, + camera: i.camera, + duration: i.duration ? +i.duration : 0, + frameMode: i.frameMode, + prompt: i.prompt, + lines: i.lines, + sound: i.sound, + associateAssetsIds: assets2StoryboardMap[i.id!] ?? [], + src: i.filePath, + state: i.state, + }); + + // 保持旧数据顺序,新增的追加到最后 + const usedIds = new Set(); + const orderedStoryboard: any[] = []; + + // 1. 按旧数据顺序遍历,若数据库中仍存在则合并更新 + if (Array.isArray(flowData.storyboard)) { + flowData.storyboard.forEach((s: any) => { + const dbItem = dbStoryboardMap[s.id]; + if (dbItem) { + orderedStoryboard.push(buildStoryboardItem(dbItem, s)); + usedIds.add(s.id); + } + }); + } + + // 2. 数据库中新增的(旧数据中没有的)追加到最后 + storyboardData.forEach((i) => { + if (!usedIds.has(i.id!)) { + orderedStoryboard.push(buildStoryboardItem(i)); + } + }); + + flowData.storyboard = orderedStoryboard; res.status(200).send(success(flowData)); } catch (err) { - res.status(200).send(error()); + res.status(400).send(error()); } } }, diff --git a/src/routes/production/storyboard/previewImage.ts b/src/routes/production/storyboard/previewImage.ts new file mode 100644 index 0000000..84e0611 --- /dev/null +++ b/src/routes/production/storyboard/previewImage.ts @@ -0,0 +1,115 @@ +import express from "express"; +import u from "@/utils"; +import { z } from "zod"; +import sharp from "sharp"; +import { success } from "@/lib/responseFormat"; +import { validateFields } from "@/middleware/middleware"; +const router = express.Router(); + +export default router.post( + "/", + validateFields({ + storyboardIds: z.array(z.number()), + }), + async (req, res) => { + const { storyboardIds } = req.body; + const storyboardImage = await u.db("o_storyboard").whereIn("id", storyboardIds).select("id", "filePath"); + + // 按 storyboardIds 顺序构建 filePath 映射 + const filePathMap: Record = {}; + storyboardImage.forEach((i) => { + filePathMap[i.id!] = i.filePath || ""; + }); + const orderedFilePaths = storyboardIds.map((id: number) => filePathMap[id]); + + // 读取所有图片 buffer 并获取元数据 + const loaded = await Promise.all( + orderedFilePaths.map(async (filePath: string) => { + if (!filePath) return null; + const buffer = await u.oss.getFile(filePath); + const metadata = await sharp(buffer).metadata(); + return { buffer, width: metadata.width || 0, height: metadata.height || 0 }; + }), + ); + + // 过滤掉无效图片 + const validImages = loaded.filter((img): img is NonNullable => img !== null && img.width > 0 && img.height > 0); + if (validImages.length === 0) { + return res.status(200).send(success(null)); + } + + // 计算网格布局 + const cols = Math.min(5, validImages.length); + const rows = Math.ceil(validImages.length / cols); + + const colWidths: number[] = Array(cols).fill(0); + const rowHeights: number[] = Array(rows).fill(0); + validImages.forEach((img, idx) => { + const c = idx % cols; + const r = Math.floor(idx / cols); + colWidths[c] = Math.max(colWidths[c], img.width); + rowHeights[r] = Math.max(rowHeights[r], img.height); + }); + + const canvasWidth = colWidths.reduce((a, b) => a + b, 0); + const canvasHeight = rowHeights.reduce((a, b) => a + b, 0); + + // 为每张图片生成带标号的合成层 + const compositeInputs: sharp.OverlayOptions[] = []; + + for (let i = 0; i < validImages.length; i++) { + const img = validImages[i]; + const c = i % cols; + const r = Math.floor(i / cols); + const x = colWidths.slice(0, c).reduce((a, b) => a + b, 0); + const y = rowHeights.slice(0, r).reduce((a, b) => a + b, 0); + + // 添加图片层 + compositeInputs.push({ + input: img.buffer, + left: x, + top: y, + }); + + // 生成标号标签 SVG + const label = `S${String(i + 1).padStart(2, "0")}`; + const fontSize = Math.max(14, Math.min(img.width, img.height) * 0.06); + const padding = Math.round(fontSize * 0.4); + // 估算文字宽度(等宽近似) + const textWidth = Math.round(label.length * fontSize * 0.65); + const bgW = textWidth + padding * 2; + const bgH = Math.round(fontSize) + padding * 2; + + const labelSvg = Buffer.from( + ` + + ${label} + `, + ); + + compositeInputs.push({ + input: labelSvg, + left: x + 4, + top: y + 4, + }); + } + + // 使用 sharp 创建画布并合成 + const resultBuffer = await sharp({ + create: { + width: canvasWidth, + height: canvasHeight, + channels: 4, + background: { r: 255, g: 255, b: 255, alpha: 1 }, + }, + }) + .composite(compositeInputs) + .png() + .toBuffer(); + + const base64 = resultBuffer.toString("base64"); + const dataUrl = `data:image/png;base64,${base64}`; + + return res.status(200).send(success(dataUrl)); + }, +); diff --git a/src/routes/production/workbench/getChatLines.ts b/src/routes/production/workbench/getChatLines.ts index cd83467..3aba49a 100644 --- a/src/routes/production/workbench/getChatLines.ts +++ b/src/routes/production/workbench/getChatLines.ts @@ -42,6 +42,8 @@ async function getLines(prompt: string) { }), }), }); + console.log("%c Line:36 🍉 resText", "background:#e41a6a", resText); + const parseLines = JSON.parse(resText.text); const chatLines = parseLines.elements.map((i: any) => i.lines); return chatLines; diff --git a/src/routes/project/addProject.ts b/src/routes/project/addProject.ts index 9a8f597..d3f33c3 100644 --- a/src/routes/project/addProject.ts +++ b/src/routes/project/addProject.ts @@ -15,9 +15,10 @@ export default router.post( type: z.string(), artStyle: z.string(), videoRatio: z.string(), + model: z.string(), }), async (req, res) => { - const { projectType, name, intro, type, artStyle, videoRatio } = req.body; + const { projectType, name, intro, type, artStyle, videoRatio, model } = req.body; await u.db("o_project").insert({ projectType, @@ -27,6 +28,7 @@ export default router.post( artStyle, videoRatio, userId: 1, + model, createTime: Date.now(), }); diff --git a/src/routes/project/editProject.ts b/src/routes/project/editProject.ts index cafcb5e..64f224f 100644 --- a/src/routes/project/editProject.ts +++ b/src/routes/project/editProject.ts @@ -15,9 +15,10 @@ export default router.post( type: z.string(), artStyle: z.string(), videoRatio: z.string(), + model: z.string(), }), async (req, res) => { - const { id, name, intro, type, artStyle, videoRatio } = req.body; + const { id, name, intro, type, artStyle, videoRatio, model } = req.body; await u.db("o_project").where("id", id).update({ name, @@ -25,6 +26,7 @@ export default router.post( type, artStyle, videoRatio, + model, }); res.status(200).send(success({ message: "新增项目成功" })); diff --git a/src/types/database.d.ts b/src/types/database.d.ts index b6d8661..1be7b18 100644 --- a/src/types/database.d.ts +++ b/src/types/database.d.ts @@ -1,4 +1,4 @@ -// @db-hash d7bc24a5440e2cc7136872da7ed6c4c7 +// @db-hash 2b2f9f6242d2d20e89412ba5117415df //该文件由脚本自动生成,请勿手动修改 export interface memories { @@ -53,7 +53,7 @@ export interface o_assets { } export interface o_assets2Storyboard { 'assetId'?: number; - 'storyboardId': number; + 'storyboardId'?: number; } export interface o_event { 'createTime'?: number | null; @@ -109,6 +109,7 @@ export interface o_project { 'createTime'?: number | null; 'id'?: number | null; 'intro'?: string | null; + 'model'?: string | null; 'name'?: string | null; 'projectType'?: string | null; 'type'?: string | null;