修复新增分镜未绑定问题

This commit is contained in:
zhishi 2026-03-25 22:30:38 +08:00
parent 814a2afebe
commit 95dfc38f3a
8 changed files with 257 additions and 88 deletions

2
env/.env.dev vendored
View File

@ -1,4 +1,4 @@
NODE_ENV=dev NODE_ENV=dev
PORT=10588 PORT=10588
OSSURL=http://127.0.0.1:10588/ OSSURL=http://192.168.0.74:10588/

View File

@ -350,7 +350,6 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
await u.db("o_storyboardFlow").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 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)); const storyboardData = flowData["storyboard"].filter((item) => !ids.includes(item.id));
socket.emit("setFlowData", { key: "storyboard", value: storyboardData }); socket.emit("setFlowData", { key: "storyboard", value: storyboardData });
return true; return true;
}, },
@ -408,27 +407,26 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
return true; return true;
}, },
}), }),
// todo 提示词待调
//todo referenceIds 图片未使用 提示词待调
generate_storyboard_images: tool({ generate_storyboard_images: tool({
description: `生成一组图片任务,支持图片间的依赖关系(以图生图) description: `生成一组图片任务,支持图片间的依赖关系(以图生图),基于有向无环图(DAG)拓扑排序执行
- images: 图片任务数组 - images: 图片任务数组
- id: 图片唯一标识符 - id: 图片唯一标识符id
- prompt: 图片生成提示词 - prompt: 图片生成提示词
- referenceIds: 依赖的参考图id数组[] - referenceIds: 依赖的参考图id数组[]
- assetIds: 参考的资产图id数组 - assetIds: 参考的资产图id数组
1. referenceIds中的id必须存在于images数组中 1. referenceIds中的id必须存在于images数组中
2. A依赖BB依赖A 2. A依赖BB依赖A
3. 3.
images: [ images: [
{id: "cat", prompt: "一只橘猫", referenceIds: [], assetIds: []}, {id: 1, prompt: "一只橘猫", referenceIds: [], assetIds: []},
{id: "dog", prompt: "风格相同的金毛犬", referenceIds: ["cat"], assetIds: []} {id: 2, prompt: "风格相同的金毛犬", referenceIds: [1], assetIds: []}
]`, ]`,
inputSchema: z.object({ inputSchema: z.object({
images: z.array( images: z.array(
@ -441,53 +439,141 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
), ),
}), }),
execute: async ({ images }) => { 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[]>();
for (const item of images) {
// 只统计本批次内的依赖作为入度
const internalDeps = item.referenceIds.filter((refId) => taskIds.has(refId));
inDegree.set(item.id, internalDeps.length);
for (const depId of internalDeps) {
if (!adjacency.has(depId)) adjacency.set(depId, []);
adjacency.get(depId)!.push(item.id);
}
}
// 拓扑排序,按层级分组(同层可并行)
const levels: number[][] = [];
let queue = images.filter((item) => (inDegree.get(item.id) ?? 0) === 0).map((item) => item.id);
const visited = new Set<number>();
while (queue.length > 0) {
levels.push([...queue]);
const nextQueue: number[] = [];
for (const nodeId of queue) {
visited.add(nodeId);
for (const childId of adjacency.get(nodeId) ?? []) {
inDegree.set(childId, (inDegree.get(childId) ?? 1) - 1);
if (inDegree.get(childId) === 0) {
nextQueue.push(childId);
}
}
}
queue = nextQueue;
}
// 循环依赖检测
if (visited.size !== images.length) {
const cyclicIds = images.filter((item) => !visited.has(item.id)).map((item) => item.id);
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 skill = await useSkill("universal-agent");
const projectData = await u.db("o_project").where("id", resTool.data.projectId).select("videoRatio").first(); const projectData = await u.db("o_project").where("id", resTool.data.projectId).select("videoRatio").first();
for (const item of images) { const imageModel = resTool.data.imageModel;
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({ const generateOneImage = async (item: (typeof images)[0]) => {
systemPrompt: skill.prompt, resTool.systemMessage(`正在生成分镜 id:${item.id} 图片`);
prompt: item.prompt, // 更新数据库状态为生成中
imageBase64: [...(await getAssetsImageBase64(item.assetIds ?? [])), ...(await getStoryboardImageBase64(item.referenceIds))], await u.db("o_storyboard").where("id", item.id).update({ state: "生成中" });
size: imageModel?.quality, // 更新前端为生成中
aspectRatio: (projectData?.videoRatio as `${number}:${number}`) ?? "16:9", socket.emit("setFlowData", {
taskClass: "生成图片", key: "setStoryboardImage",
describe: "分镜图片生成", value: { ...item, id: item.id, src: "", state: "生成中", referenceIds: item.referenceIds },
relatedObjects: "hhhh", });
projectId: resTool.data.projectId,
}) // 获取参考图base64包括资产图和已生成的分镜参考图
.then(async (imageCls) => { const [assetsBase64, referenceBase64] = await Promise.all([
const savePath = `/${resTool.data.projectId}/storyboard/${u.uuid()}.jpg`; getAssetsImageBase64(item.assetIds ?? []),
await imageCls.save(savePath); getStoryboardImageBase64(item.referenceIds),
const obj = { ]);
...item,
id: item.id, const imageCls = await u.Ai.Image(imageModel?.modelId).run({
src: await u.oss.getFileUrl(savePath), systemPrompt: skill.prompt,
state: "已完成", prompt: item.prompt,
}; imageBase64: [...assetsBase64, ...referenceBase64],
// 更新对应分镜状态 size: imageModel?.quality,
await u.db("o_storyboard").where("id", item.id).update({ state: "已完成", filePath: savePath }); aspectRatio: (projectData?.videoRatio as `${number}:${number}`) ?? "16:9",
// 前端对话框提示 taskClass: "生成图片",
resTool.systemMessage(`分镜 id:${item.id} 图片生成完成`); describe: "分镜图片生成",
// 更新前端界面展示 relatedObjects: "hhhh",
socket.emit("setFlowData", { key: "setStoryboardImage", value: obj }); projectId: resTool.data.projectId,
}); });
//更新前端为生成中
socket.emit("setFlowData", { key: "setStoryboardImage", value: { ...item, id: item.id, src: "", state: "生成中" } }); 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 "分镜图片生成中";
return "分镜图片生成完成";
}, },
}), }),
//todo 图片是否需要参考 原资产 提示词待调 //todo 提示词待调
generate_assets_images: tool({ generate_assets_images: tool({
description: ` description: `
@ -505,15 +591,29 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
z.object({ z.object({
assetId: z.number().describe("衍生资产id"), assetId: z.number().describe("衍生资产id"),
prompt: z.string().describe("提示词"), prompt: z.string().describe("提示词"),
refenceAssetsId: z.array(z.number()).describe("参考[资产]id注意资产和衍生资产为两种类型衍生资产归类在资产下面"),
}), }),
), ),
}), }),
execute: async ({ images }) => { execute: async ({ images }) => {
const skill = await useSkill("universal-agent"); const skill = await useSkill("universal-agent");
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; const imageModel = resTool.data.imageModel;
for (const item of images) { for (const item of assetsImage) {
const [imageId] = await u.db("o_image").insert({ const [imageId] = await u.db("o_image").insert({
// 数据库插入图片记录 // 数据库插入图片记录
assetsId: item.assetId, assetsId: item.assetId,
@ -525,7 +625,7 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
.run({ .run({
// systemPrompt: skill.prompt, // systemPrompt: skill.prompt,
prompt: item.prompt, prompt: item.prompt,
imageBase64: await getAssetsImageBase64(item.refenceAssetsId ?? []), imageBase64: await getAssetsImageBase64(item.id ? [item.id] : []),
size: imageModel?.quality, size: imageModel?.quality,
aspectRatio: "16:9", aspectRatio: "16:9",
taskClass: "生成图片", taskClass: "生成图片",
@ -551,7 +651,6 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
//通知前端更新状态 //通知前端更新状态
socket.emit("setFlowData", { key: "setAssetsImage", value: { ...item, id: item.assetId, src: "", state: "生成中" } }); socket.emit("setFlowData", { key: "setAssetsImage", value: { ...item, id: item.assetId, src: "", state: "生成中" } });
} }
console.log("[tools] generate_assets_images", images);
return "资产生成中"; return "资产生成中";
}, },
}), }),

View File

@ -108,6 +108,10 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
if (assetsList && assetsList.length) { if (assetsList && assetsList.length) {
const assetId = []; const assetId = [];
for (const i of assetsList) { for (const i of assetsList) {
if (i.id) {
assetId.push(i.id);
continue;
}
const [id] = await u.db("o_assets").insert({ const [id] = await u.db("o_assets").insert({
name: i.name, name: i.name,
prompt: i.prompt, prompt: i.prompt,

View File

@ -13,9 +13,10 @@ export default router.post(
imageUrl: z.string(), imageUrl: z.string(),
id: z.number().nullable().optional(), id: z.number().nullable().optional(),
type: z.enum(["role", "scene", "storyboard", "clip", "tool"]), type: z.enum(["role", "scene", "storyboard", "clip", "tool"]),
episodesId: z.number(),
}), }),
async (req, res) => { async (req, res) => {
const { edges, nodes, imageUrl, id, type } = req.body; const { edges, nodes, imageUrl, id, type, episodesId } = req.body;
let imagePath = ""; let imagePath = "";
try { try {
imagePath = new URL(imageUrl).pathname; imagePath = new URL(imageUrl).pathname;
@ -56,6 +57,7 @@ export default router.post(
} else { } else {
const [storyboardId] = await u.db("o_storyboard").insert({ const [storyboardId] = await u.db("o_storyboard").insert({
filePath: imagePath, filePath: imagePath,
scriptId: episodesId,
createTime: Date.now(), createTime: Date.now(),
}); });
insertFlowId = storyboardId; insertFlowId = storyboardId;

View File

@ -14,9 +14,10 @@ export default router.post(
imageUrl: z.string(), imageUrl: z.string(),
type: z.enum(["role", "scene", "storyboard", "clip", "tool"]), type: z.enum(["role", "scene", "storyboard", "clip", "tool"]),
flowId: z.number(), flowId: z.number(),
episodesId: z.number(),
}), }),
async (req, res) => { 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) => { nodes.forEach((node: any) => {
if (node.type == "upload") { if (node.type == "upload") {
node.data.image = node.data.image ? new URL(node.data.image).pathname : ""; 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; imagePath = new URL(imageUrl).pathname;
} catch (e) {} } catch (e) {}
if (imagePath) { if (imagePath) {
console.log("%c Line:34 🍰", "background:#33a5ff");
if (type == "storyboard") { if (type == "storyboard") {
await u.db("o_storyboard").where("id", id).update({ await u.db("o_storyboard").where("id", id).update({
filePath: imagePath, filePath: imagePath,

View File

@ -86,34 +86,102 @@ export default router.post(
return res.status(200).send(success(flowData)); return res.status(200).send(success(flowData));
} else { } else {
try { try {
const flowData = JSON.parse(sqlData!.data ?? "{}"); const storyboardData = await u.db("o_storyboard").where("scriptId", episodesId);
flowData.assets = await Promise.all( console.log("%c Line:90 🍡 storyboardData", "background:#ed9ec7", storyboardData.length);
assetsData.map(async (item) => ({ await Promise.all(
id: item.id, storyboardData.map(async (i) => {
name: item.name ?? "", if (i.filePath) {
type: item.type ?? "", try {
prompt: item.prompt ?? "", i.filePath = await u.oss.getFileUrl(i.filePath);
desc: item.describe ?? "", } catch {
src: item.filePath && (await u.oss.getFileUrl(item.filePath!)), i.filePath = "";
derive: await Promise.all( }
childAssetsData } else {
.filter((child) => child.assetsId === item.id) i.filePath = "";
.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 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矫正状态值
})),
),
};
}),
);
// 将原有 flowData.storyboard 按 id 建立索引,以便后续合并保留旧字段
const existingStoryboardMap: Record<number, any> = {};
if (Array.isArray(flowData.storyboard)) {
flowData.storyboard.forEach((s: any) => {
existingStoryboardMap[s.id] = s;
});
}
flowData.storyboard = storyboardData.map((i) => {
const existing = existingStoryboardMap[i.id!] ?? {};
return {
...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,
};
});
res.status(200).send(success(flowData)); res.status(200).send(success(flowData));
} catch (err) { } catch (err) {
res.status(200).send(error()); res.status(400).send(error());
} }
} }
}, },

View File

@ -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 parseLines = JSON.parse(resText.text);
const chatLines = parseLines.elements.map((i: any) => i.lines); const chatLines = parseLines.elements.map((i: any) => i.lines);
return chatLines; return chatLines;

View File

@ -1,4 +1,4 @@
// @db-hash 62a748aea9d1ecee865c4cf05add24fc // @db-hash a3673cf3a1d1c9cbf22ae3cfff196a71
//该文件由脚本自动生成,请勿手动修改 //该文件由脚本自动生成,请勿手动修改
export interface memories { export interface memories {
@ -53,7 +53,7 @@ export interface o_assets {
} }
export interface o_assets2Storyboard { export interface o_assets2Storyboard {
'assetId'?: number; 'assetId'?: number;
'storyboardId': number; 'storyboardId'?: number;
} }
export interface o_event { export interface o_event {
'createTime'?: number | null; 'createTime'?: number | null;
@ -149,11 +149,6 @@ export interface o_storyboard {
'state'?: string | null; 'state'?: string | null;
'title'?: string | null; 'title'?: string | null;
} }
export interface o_storyboardFlow {
'flowData': string;
'id'?: number;
'storyboardId': number;
}
export interface o_tasks { export interface o_tasks {
'describe'?: string | null; 'describe'?: string | null;
'id'?: number; 'id'?: number;
@ -224,7 +219,6 @@ export interface DB {
"o_scriptAssets": o_scriptAssets; "o_scriptAssets": o_scriptAssets;
"o_setting": o_setting; "o_setting": o_setting;
"o_storyboard": o_storyboard; "o_storyboard": o_storyboard;
"o_storyboardFlow": o_storyboardFlow;
"o_tasks": o_tasks; "o_tasks": o_tasks;
"o_user": o_user; "o_user": o_user;
"o_vendorConfig": o_vendorConfig; "o_vendorConfig": o_vendorConfig;