Merge branch '108' of https://github.com/HBAI-Ltd/Toonflow-app into 108
# Conflicts: # src/agents/productionAgent/tools.ts # src/router.ts # src/types/database.d.ts
This commit is contained in:
commit
35993e7bfe
@ -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)];
|
||||
|
||||
@ -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<typeof deriveAssetSchema>[];
|
||||
const { projectId, scriptId } = resTool.data;
|
||||
const startTime = Date.now();
|
||||
|
||||
// 并行插入所有 o_assets 记录
|
||||
await Promise.all(
|
||||
setData.map(async (i) => {
|
||||
const [insertedId] = await u.db("o_assets").insert({
|
||||
assetsId: +i.assetsId || null,
|
||||
projectId,
|
||||
name: i.name,
|
||||
type: i.type,
|
||||
prompt: i.prompt,
|
||||
describe: i.desc,
|
||||
startTime,
|
||||
});
|
||||
i.id = insertedId;
|
||||
}),
|
||||
);
|
||||
|
||||
// 批量插入 o_scriptAssets
|
||||
await u.db("o_scriptAssets").insert(setData.map((i) => ({ scriptId, assetId: i.id })));
|
||||
|
||||
const watiAddAssetsMap: Record<number, z.infer<typeof deriveAssetSchema>[]> = {};
|
||||
setData.forEach((i) => {
|
||||
if (watiAddAssetsMap[i.assetsId]) {
|
||||
watiAddAssetsMap[i.assetsId].push(i);
|
||||
} else {
|
||||
watiAddAssetsMap[i.assetsId] = [i];
|
||||
}
|
||||
}
|
||||
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<number, z.infer<typeof deriveAssetSchema>[]> = {};
|
||||
value.forEach((i) => {
|
||||
if (updateAssetsMap[i.assetsId]) {
|
||||
updateAssetsMap[i.assetsId].push(i);
|
||||
} else {
|
||||
updateAssetsMap[i.assetsId] = [i];
|
||||
}
|
||||
});
|
||||
const flowData: FlowData = await new Promise((resolve) => socket.emit("getFlowData", { key: "assets" }, (res: any) => resolve(res)));
|
||||
const assetsData = flowData.assets;
|
||||
// 将 derive 中已存在的条目替换为更新后的数据
|
||||
assetsData.forEach((asset) => {
|
||||
if (updateAssetsMap[asset.id]) {
|
||||
const updatedMap = Object.fromEntries(updateAssetsMap[asset.id].map((d) => [d.id, d]));
|
||||
asset.derive = (asset.derive || []).map((d) => updatedMap[d.id] ?? d);
|
||||
}
|
||||
});
|
||||
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<typeof storyboardSchema>[];
|
||||
for (const item of setData) {
|
||||
item.src = "";
|
||||
const [insertedId] = await u.db("o_storyboard").insert({
|
||||
title: item.title,
|
||||
prompt: item.prompt,
|
||||
description: item.description,
|
||||
frameMode: item.frameMode,
|
||||
duration: String(item.duration),
|
||||
camera: item.camera,
|
||||
sound: item.sound,
|
||||
lines: item.lines,
|
||||
state: "未生成",
|
||||
scriptId: resTool.data.scriptId,
|
||||
});
|
||||
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<number, number>();
|
||||
// adjacency: 被依赖者 -> 依赖它的节点列表
|
||||
const adjacency = new Map<number, number[]>();
|
||||
|
||||
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<number>();
|
||||
while (queue.length > 0) {
|
||||
levels.push([...queue]);
|
||||
const nextQueue: number[] = [];
|
||||
for (const nodeId of queue) {
|
||||
visited.add(nodeId);
|
||||
for (const childId of adjacency.get(nodeId) ?? []) {
|
||||
inDegree.set(childId, (inDegree.get(childId) ?? 1) - 1);
|
||||
if (inDegree.get(childId) === 0) {
|
||||
nextQueue.push(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
queue = nextQueue;
|
||||
}
|
||||
// 循环依赖检测
|
||||
if (visited.size !== images.length) {
|
||||
const cyclicIds = images.filter((item) => !visited.has(item.id)).map((item) => item.id);
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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)];
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -30,6 +30,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
|
||||
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<void> =>
|
||||
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<void> =>
|
||||
builder: (table) => {
|
||||
table.integer("storyboardId").notNullable();
|
||||
table.integer("assetId").notNullable();
|
||||
table.primary(["assetId", "assetId"]);
|
||||
table.primary(["storyboardId", "assetId"]);
|
||||
table.unique(["storyboardId", "assetId"]);
|
||||
},
|
||||
},
|
||||
|
||||
162
src/router.ts
162
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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 }));
|
||||
},
|
||||
);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<number, number[]> = {};
|
||||
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<number, any> = {};
|
||||
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<number, any> = {};
|
||||
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<number, (typeof storyboardData)[number]> = {};
|
||||
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<number>();
|
||||
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());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
115
src/routes/production/storyboard/previewImage.ts
Normal file
115
src/routes/production/storyboard/previewImage.ts
Normal file
@ -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<number, string> = {};
|
||||
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<typeof img> => 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(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="${bgW}" height="${bgH}">
|
||||
<rect x="0" y="0" width="${bgW}" height="${bgH}" rx="4" ry="4" fill="rgba(0,0,0,0.55)"/>
|
||||
<text x="${padding}" y="${padding + fontSize * 0.85}" font-family="Arial, sans-serif" font-weight="bold" font-size="${fontSize}" fill="#fff">${label}</text>
|
||||
</svg>`,
|
||||
);
|
||||
|
||||
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));
|
||||
},
|
||||
);
|
||||
@ -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;
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@ -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: "新增项目成功" }));
|
||||
|
||||
5
src/types/database.d.ts
vendored
5
src/types/database.d.ts
vendored
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user