# Conflicts:
#	src/types/database.d.ts
This commit is contained in:
ACT丶流星雨 2026-03-25 12:22:17 +08:00
commit 4bcc1947bf
11 changed files with 149 additions and 82 deletions

View File

@ -33,7 +33,7 @@ description: >
1. 调用 `get_flowData` 分别获取 `script`(剧本)和 `assets`(现有资产列表) 1. 调用 `get_flowData` 分别获取 `script`(剧本)和 `assets`(现有资产列表)
2. 根据[分镜表生成](references/storyboard-generation.md)文档中的拆分原则和字段填写指引将剧本拆分为分镜填写每条分镜的所有字段id、title、description、camera、duration、frameMode、prompt、lines、sound、associateAssetsIds 2. 根据[分镜表生成](references/storyboard-generation.md)文档中的拆分原则和字段填写指引将剧本拆分为分镜填写每条分镜的所有字段id、title、description、camera、duration、frameMode、prompt、lines、sound、associateAssetsIds
3. 调用 `set_flowData({ key: "storyboardTable", value: 分镜数组 })` 一次性保存完整分镜表 3. 调用 `set_flowData({ key: "storyboard", value: 分镜数组 })` 一次性保存完整分镜表
4. 告知用户分镜表生成完成,列出分镜概要(总条数、主要场景) 4. 告知用户分镜表生成完成,列出分镜概要(总条数、主要场景)
5. **询问用户是否需要生成分镜图片** 5. **询问用户是否需要生成分镜图片**
- 如果用户确认需要,调用 `generate_storyboard_images({ script: 剧本文本 })` 生成分镜图 - 如果用户确认需要,调用 `generate_storyboard_images({ script: 剧本文本 })` 生成分镜图

View File

@ -1,4 +1,4 @@
# 分镜面板生成(从剧本 + 资产 → storyboardTable # 分镜面板生成(从剧本 + 资产 → storyboard
本指南只做一件事: 本指南只做一件事:
根据剧本内容和已有资产,将剧本拆分为一系列分镜,生成结构化的分镜面板。 根据剧本内容和已有资产,将剧本拆分为一系列分镜,生成结构化的分镜面板。
@ -18,7 +18,7 @@
```ts ```ts
set_flowData({ set_flowData({
key: "storyboardTable", key: "storyboard",
value: [ value: [
{ {
id: 1, id: 1,
@ -185,7 +185,7 @@ set_flowData({
```ts ```ts
set_flowData({ set_flowData({
key: "storyboardTable", key: "storyboard",
value: [ value: [
{ {
id: 1, id: 1,
@ -232,7 +232,7 @@ set_flowData({
1. `get_flowData("script")` — 获取剧本内容 1. `get_flowData("script")` — 获取剧本内容
2. `get_flowData("assets")` — 获取已有资产列表 2. `get_flowData("assets")` — 获取已有资产列表
3. 分析剧本,按照拆分原则划分分镜,并为每条分镜填写所有字段 3. 分析剧本,按照拆分原则划分分镜,并为每条分镜填写所有字段
4. 调用 `set_flowData({ key: "storyboardTable", value: 分镜数组 })` 一次性保存完整分镜面板 4. 调用 `set_flowData({ key: "storyboard", value: 分镜数组 })` 一次性保存完整分镜面板
5. 向用户汇报分镜面板概要(总共多少条分镜,覆盖的场景概括) 5. 向用户汇报分镜面板概要(总共多少条分镜,覆盖的场景概括)
6. **询问用户是否需要生成分镜图片** 6. **询问用户是否需要生成分镜图片**
- 如果用户确认,调用 `generate_storyboard_images({ script: 剧本文本 })` 生成分镜图 - 如果用户确认,调用 `generate_storyboard_images({ script: 剧本文本 })` 生成分镜图

View File

@ -11,7 +11,7 @@ export const deriveAssetSchema = z.object({
prompt: z.string().describe("生成提示词"), prompt: z.string().describe("生成提示词"),
name: z.string().describe("衍生资产名称"), name: z.string().describe("衍生资产名称"),
desc: z.string().describe("衍生资产描述"), desc: z.string().describe("衍生资产描述"),
src: z.string().describe("衍生资产资源路径"), src: z.string().nullable().describe("衍生资产资源路径"),
state: z.enum(["未生成", "生成中", "已完成", "生成失败"]).describe("衍生资产生成状态"), state: z.enum(["未生成", "生成中", "已完成", "生成失败"]).describe("衍生资产生成状态"),
type: z.enum(["role", "tool", "scene", "clip"]).describe("衍生资产类型"), type: z.enum(["role", "tool", "scene", "clip"]).describe("衍生资产类型"),
}); });
@ -24,7 +24,7 @@ export const assetItemSchema = z.object({
derive: z.array(deriveAssetSchema).describe("衍生资产列表"), derive: z.array(deriveAssetSchema).describe("衍生资产列表"),
}); });
export const storyboardSchema = z.object({ export const storyboardSchema = z.object({
id: z.number().optional().describe("分镜ID,未从工作区获得的分镜列表视为需要新增;如需新增则为空"), id: z.number().optional().describe("分镜ID,未从工作区获得的分镜面板视为需要新增;如需新增则为空"),
title: z.string().describe("分镜标题"), title: z.string().describe("分镜标题"),
description: z.string().describe("分镜描述"), description: z.string().describe("分镜描述"),
camera: z.string().describe("镜头信息"), camera: z.string().describe("镜头信息"),
@ -52,8 +52,8 @@ export const flowDataSchema = z.object({
script: z.string().describe("剧本内容"), script: z.string().describe("剧本内容"),
scriptPlan: z.string().describe("拍摄计划"), scriptPlan: z.string().describe("拍摄计划"),
assets: z.array(assetItemSchema).describe("衍生资产"), assets: z.array(assetItemSchema).describe("衍生资产"),
storyboardTable: z.string().describe("分镜面板"), storyboardTable: z.string().describe("分镜"),
storyboard: z.array(storyboardSchema).describe("分镜列表"), storyboard: z.array(storyboardSchema).describe("分镜面板"),
workbench: workbenchDataSchema.describe("工作台配置"), workbench: workbenchDataSchema.describe("工作台配置"),
poster: z poster: z
.object({ .object({
@ -168,7 +168,6 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
describe: sub.desc, describe: sub.desc,
startTime: Date.now(), startTime: Date.now(),
}); });
console.log("%c Line:141 🍑 resTool.data.scriptId", "background:#ea7e5c", resTool.data.scriptId);
await u.db("o_scriptAssets").insert({ await u.db("o_scriptAssets").insert({
scriptId: resTool.data.scriptId, scriptId: resTool.data.scriptId,
assetId: insertedId, assetId: insertedId,
@ -183,21 +182,21 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
}, },
}), }),
set_flowData_storyboardTable: tool({ set_flowData_storyboardTable: tool({
description: "保存分镜模板到工作区", description: "保存分镜到工作区",
inputSchema: z.object({ value: flowDataSchema.shape.storyboardTable }), inputSchema: z.object({ value: flowDataSchema.shape.storyboardTable }),
execute: async ({ value }) => { execute: async ({ value }) => {
console.log("[tools] set_flowData storyboardTable", value); console.log("[tools] set_flowData storyboardTable", value);
resTool.systemMessage("正在保存 分镜面板 数据..."); resTool.systemMessage("正在保存 分镜 数据...");
socket.emit("setFlowData", { key: "storyboardTable", value }); socket.emit("setFlowData", { key: "storyboardTable", value });
return true; return true;
}, },
}), }),
set_flowData_storyboard: tool({ set_flowData_storyboard: tool({
description: "保存分镜列表到工作区", description: "保存分镜面板到工作区",
inputSchema: z.object({ value: flowDataSchema.shape.storyboard }), inputSchema: z.object({ value: flowDataSchema.shape.storyboard }),
execute: async ({ value }) => { execute: async ({ value }) => {
console.log("[tools] set_flowData storyboard", value); console.log("[tools] set_flowData storyboard", value);
resTool.systemMessage("正在保存 分镜列表 数据..."); resTool.systemMessage("正在保存 分镜面板 数据...");
for (const item of value) { for (const item of value) {
if (!item.id) { if (!item.id) {
const [insertedId] = await u.db("o_storyboard").insert({ const [insertedId] = await u.db("o_storyboard").insert({
@ -211,6 +210,7 @@ export default (resTool: ResTool, toolsNames?: string[]) => {
sound: item.sound, sound: item.sound,
lines: item.lines, lines: item.lines,
state: "未生成", state: "未生成",
scriptId: resTool.data.scriptId,
}); });
if (item.associateAssetsIds.length) { if (item.associateAssetsIds.length) {
await u.db("o_assets2Storyboard").insert(item.associateAssetsIds.map((i) => ({ storyboardId: insertedId, assetId: i }))); await u.db("o_assets2Storyboard").insert(item.associateAssetsIds.map((i) => ({ storyboardId: insertedId, assetId: i })));

View File

@ -414,13 +414,14 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
table.index(["isolationKey", "summarized"]); table.index(["isolationKey", "summarized"]);
}, },
}, },
//分镜工作流表 //图片工作流表
{ {
name: "o_storyboardFlow", name: "o_imageFlow",
builder: (table) => { builder: (table) => {
table.integer("id").notNullable(); table.integer("id").notNullable();
table.text("flowData").notNullable(); table.text("flowData").notNullable();
table.integer("storyboardId").notNullable(); table.integer("storyboardId");
table.integer("assetsId");
table.primary(["id"]); table.primary(["id"]);
table.unique(["id"]); table.unique(["id"]);
}, },

View File

@ -1,4 +1,4 @@
// @routes-hash 074af9af2c664d3497c2c676a3423399 // @routes-hash bf3c43509342cfaa6f58c3551570331d
import { Express } from "express"; import { Express } from "express";
import route1 from "./routes/agents/clearMemory"; import route1 from "./routes/agents/clearMemory";
@ -40,10 +40,10 @@ import route36 from "./routes/novel/updateNovel";
import route37 from "./routes/other/deleteAllData"; import route37 from "./routes/other/deleteAllData";
import route38 from "./routes/other/getCaptcha"; import route38 from "./routes/other/getCaptcha";
import route39 from "./routes/production/assets/getAssetsData"; import route39 from "./routes/production/assets/getAssetsData";
import route40 from "./routes/production/editStoryboard/generateStoryboardImage"; import route40 from "./routes/production/editImage/generateFlowImage";
import route41 from "./routes/production/editStoryboard/getStoryboardFlow"; import route41 from "./routes/production/editImage/getImageFlow";
import route42 from "./routes/production/editStoryboard/saveStoryboardFlow"; import route42 from "./routes/production/editImage/saveImageFlow";
import route43 from "./routes/production/editStoryboard/updateStoryboardFlow"; import route43 from "./routes/production/editImage/updateImageFlow";
import route44 from "./routes/production/exportImage"; import route44 from "./routes/production/exportImage";
import route45 from "./routes/production/getFlowData"; import route45 from "./routes/production/getFlowData";
import route46 from "./routes/production/getProductionData"; import route46 from "./routes/production/getProductionData";
@ -126,10 +126,10 @@ export default async (app: Express) => {
app.use("/api/other/deleteAllData", route37); app.use("/api/other/deleteAllData", route37);
app.use("/api/other/getCaptcha", route38); app.use("/api/other/getCaptcha", route38);
app.use("/api/production/assets/getAssetsData", route39); app.use("/api/production/assets/getAssetsData", route39);
app.use("/api/production/editStoryboard/generateStoryboardImage", route40); app.use("/api/production/editImage/generateFlowImage", route40);
app.use("/api/production/editStoryboard/getStoryboardFlow", route41); app.use("/api/production/editImage/getImageFlow", route41);
app.use("/api/production/editStoryboard/saveStoryboardFlow", route42); app.use("/api/production/editImage/saveImageFlow", route42);
app.use("/api/production/editStoryboard/updateStoryboardFlow", route43); app.use("/api/production/editImage/updateImageFlow", route43);
app.use("/api/production/exportImage", route44); app.use("/api/production/exportImage", route44);
app.use("/api/production/getFlowData", route45); app.use("/api/production/getFlowData", route45);
app.use("/api/production/getProductionData", route46); app.use("/api/production/getProductionData", route46);

View File

@ -78,9 +78,10 @@ export default router.post(
ratio: z.string(), ratio: z.string(),
prompt: z.string(), prompt: z.string(),
projectId: z.number(), projectId: z.number(),
type: z.enum(["role", "scene", "storyboard", "clip", "tool"]),
}), }),
async (req, res) => { async (req, res) => {
const { model, references = {}, quality, ratio, prompt, projectId } = req.body; const { model, references = {}, quality, ratio, prompt, projectId, type } = req.body;
const { prompt: userPrompt, images: base64Images } = await convertDirectiveAndImages(references, prompt); const { prompt: userPrompt, images: base64Images } = await convertDirectiveAndImages(references, prompt);
const imageClass = await u.Ai.Image(model).run({ const imageClass = await u.Ai.Image(model).run({
prompt: userPrompt, prompt: userPrompt,
@ -92,7 +93,7 @@ export default router.post(
relatedObjects: JSON.stringify(req.body), relatedObjects: JSON.stringify(req.body),
projectId: projectId, projectId: projectId,
}); });
const savePath = `${projectId}/storyboard/${u.uuid()}.jpg`; const savePath = `${projectId}/${type}/${u.uuid()}.jpg`;
await imageClass.save(savePath); await imageClass.save(savePath);
const url = await u.oss.getFileUrl(savePath); const url = await u.oss.getFileUrl(savePath);

View File

@ -9,12 +9,22 @@ export default router.post(
"/", "/",
validateFields({ validateFields({
id: z.number(), id: z.number(),
type: z.enum(["role", "scene", "storyboard", "clip", "tool"]),
}), }),
async (req, res) => { async (req, res) => {
const { id } = req.body; const { id, type } = req.body;
const storyboardFlowData = await u.db("o_storyboardFlow").where("storyboardId", id).first(); const imageFlowData = await u
if (storyboardFlowData?.flowData) { .db("o_imageFlow")
const parseFlow = JSON.parse(storyboardFlowData.flowData); .modify((qb) => {
if (type === "storyboard") {
qb.where("storyboardId", id);
} else {
qb.where("assetsId", id);
}
})
.first();
if (imageFlowData?.flowData) {
const parseFlow = JSON.parse(imageFlowData.flowData);
await Promise.all( await Promise.all(
parseFlow.nodes.map(async (node: any) => { parseFlow.nodes.map(async (node: any) => {
if (node.type === "upload") { if (node.type === "upload") {
@ -24,7 +34,7 @@ export default router.post(
} }
}), }),
); );
return res.status(200).send(success(parseFlow)); return res.status(200).send(success({ ...parseFlow, id: imageFlowData.id }));
} }
return res.status(200).send(success(null)); return res.status(200).send(success(null));

View File

@ -0,0 +1,71 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
edges: z.any(),
nodes: z.any(),
imageUrl: z.string(),
id: z.number().nullable().optional(),
type: z.enum(["role", "scene", "storyboard", "clip", "tool"]),
}),
async (req, res) => {
const { edges, nodes, imageUrl, id, type } = req.body;
let imagePath = "";
try {
imagePath = new URL(imageUrl).pathname;
} catch (e) {}
nodes.forEach((node: any) => {
if (node.type == "upload") {
try {
node.data.image = new URL(node.data.image).pathname;
} catch (e) {
node.data.image = "";
}
}
if (node.type == "generated") {
try {
node.data.generatedImage = new URL(node.data.generatedImage).pathname;
} catch (e) {
node.data.generatedImage = "";
}
}
});
let insertFlowId;
if (imagePath) {
if (id) {
if (type == "storyboard") {
await u.db("o_storyboard").where("id", id).update({
filePath: imagePath,
});
} else {
const [imageId] = await u.db("o_image").insert({
filePath: imagePath,
assetsId: id,
state: "已完成",
});
await u.db("o_assets").where("id", id).update({ imageId });
}
insertFlowId = id;
} else {
const [storyboardId] = await u.db("o_storyboard").insert({
filePath: imagePath,
createTime: Date.now(),
});
insertFlowId = storyboardId;
}
}
await u.db("o_imageFlow").insert({
flowData: JSON.stringify({ edges, nodes }),
...(type == "assets" ? { assetsId: insertFlowId } : { storyboardId: insertFlowId }),
});
return res.status(200).send(success());
},
);

View File

@ -12,12 +12,11 @@ export default router.post(
nodes: z.any(), nodes: z.any(),
id: z.number(), id: z.number(),
imageUrl: z.string(), imageUrl: z.string(),
type: z.enum(["role", "scene", "storyboard", "clip", "tool"]),
flowId: z.number(),
}), }),
async (req, res) => { async (req, res) => {
const { edges, nodes, id, imageUrl } = req.body; const { edges, nodes, id, imageUrl, flowId, type } = req.body;
if (!imageUrl.includes("http")) {
return res.status(400).send({ message: "图片地址不合法" });
}
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 : "";
@ -26,13 +25,29 @@ export default router.post(
node.data.generatedImage = node.data.generatedImage ? new URL(node.data.generatedImage).pathname : ""; node.data.generatedImage = node.data.generatedImage ? new URL(node.data.generatedImage).pathname : "";
} }
}); });
let imagePath = "";
try {
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,
});
} else {
const [imageId] = await u.db("o_image").insert({
filePath: imagePath,
assetsId: id,
state: "已完成",
});
await u.db("o_assets").where("id", id).update({ imageId });
}
}
await u await u
.db("o_storyboard") .db("o_imageFlow")
.where("id", id) .where("id", flowId)
.update({ filePath: new URL(imageUrl).pathname });
await u
.db("o_storyboardFlow")
.where("storyboardId", id)
.update({ .update({
flowData: JSON.stringify({ edges, nodes }), flowData: JSON.stringify({ edges, nodes }),
}); });

View File

@ -1,38 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
edges: z.any(),
nodes: z.any(),
imageUrl: z.string(),
}),
async (req, res) => {
const { edges, nodes, imageUrl } = req.body;
if (!imageUrl.includes("http")) {
return res.status(400).send({ message: "图片地址不合法" });
}
nodes.forEach((node: any) => {
if (node.type == "upload") {
node.data.image = node.data.image ? new URL(node.data.image).pathname : "";
}
if (node.type == "generated") {
node.data.generatedImage = node.data.generatedImage ? new URL(node.data.generatedImage).pathname : "";
}
});
const [id] = await u.db("o_storyboard").insert({
filePath: new URL(imageUrl).pathname,
createTime: Date.now(),
});
await u.db("o_storyboardFlow").insert({
storyboardId: id,
flowData: JSON.stringify({ edges, nodes }),
});
return res.status(200).send(success());
},
);

View File

@ -1,4 +1,4 @@
// @db-hash ae9ddbb1e0746c8b2524ce28d3cd4cfa // @db-hash 62a748aea9d1ecee865c4cf05add24fc
//该文件由脚本自动生成,请勿手动修改 //该文件由脚本自动生成,请勿手动修改
export interface memories { export interface memories {
@ -75,6 +75,12 @@ export interface o_image {
'state'?: string | null; 'state'?: string | null;
'type'?: string | null; 'type'?: string | null;
} }
export interface o_imageFlow {
'assetsId'?: number | null;
'flowData': string;
'id'?: number;
'storyboardId'?: number | null;
}
export interface o_novel { export interface o_novel {
'chapter'?: string | null; 'chapter'?: string | null;
'chapterData'?: string | null; 'chapterData'?: string | null;
@ -209,6 +215,7 @@ export interface DB {
"o_event": o_event; "o_event": o_event;
"o_eventChapter": o_eventChapter; "o_eventChapter": o_eventChapter;
"o_image": o_image; "o_image": o_image;
"o_imageFlow": o_imageFlow;
"o_novel": o_novel; "o_novel": o_novel;
"o_outline": o_outline; "o_outline": o_outline;
"o_outlineNovel": o_outlineNovel; "o_outlineNovel": o_outlineNovel;