From 558e9b95e3d2a9740f7de1bb432f2663f9adf769 Mon Sep 17 00:00:00 2001 From: zhishi <1951671751@qq.com> Date: Wed, 25 Mar 2026 23:42:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=AB=E6=A0=BC=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- env/.env.dev | 2 +- src/lib/initDB.ts | 1 + src/router.ts | 148 +++++++++--------- src/routes/production/getFlowData.ts | 61 +++++--- .../production/storyboard/previewImage.ts | 115 ++++++++++++++ 5 files changed, 233 insertions(+), 94 deletions(-) create mode 100644 src/routes/production/storyboard/previewImage.ts diff --git a/env/.env.dev b/env/.env.dev index 5b6dc86..584000d 100644 --- a/env/.env.dev +++ b/env/.env.dev @@ -1,4 +1,4 @@ NODE_ENV=dev PORT=10588 -OSSURL=http://192.168.0.74:10588/ +OSSURL=http://127.0.0.1:10588/ diff --git a/src/lib/initDB.ts b/src/lib/initDB.ts index f2b7a7a..feea1e8 100644 --- a/src/lib/initDB.ts +++ b/src/lib/initDB.ts @@ -308,6 +308,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => table.text("lines"); table.text("state"); table.text("reason"); + table.text("index"); table.integer("createTime"); table.primary(["id"]); table.unique(["id"]); diff --git a/src/router.ts b/src/router.ts index bac9636..291c400 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,4 +1,4 @@ -// @routes-hash bf3c43509342cfaa6f58c3551570331d +// @routes-hash f5f78866e59979bf30af031c9ea0de82 import { Express } from "express"; import route1 from "./routes/agents/clearMemory"; @@ -49,42 +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/vendorConfig/addVendor"; -import route77 from "./routes/setting/vendorConfig/deleteVendor"; -import route78 from "./routes/setting/vendorConfig/getVendorList"; -import route79 from "./routes/setting/vendorConfig/modelTest"; -import route80 from "./routes/setting/vendorConfig/updateVendor"; -import route81 from "./routes/task/getTaskApi"; -import route82 from "./routes/task/getTaskCategories"; -import route83 from "./routes/task/taskDetails"; -import route84 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); @@ -135,40 +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/vendorConfig/addVendor", route76); - app.use("/api/setting/vendorConfig/deleteVendor", route77); - app.use("/api/setting/vendorConfig/getVendorList", route78); - app.use("/api/setting/vendorConfig/modelTest", route79); - app.use("/api/setting/vendorConfig/updateVendor", route80); - app.use("/api/task/getTaskApi", route81); - app.use("/api/task/getTaskCategories", route82); - app.use("/api/task/taskDetails", route83); - app.use("/api/test/test", route84); + app.use("/api/production/storyboard/previewImage", route49); + app.use("/api/production/workbench/confirmSelection", route50); + app.use("/api/production/workbench/delVideo", route51); + app.use("/api/production/workbench/generateVideo", route52); + app.use("/api/production/workbench/getChatLines", route53); + app.use("/api/production/workbench/getVideoModelDetail", route54); + app.use("/api/production/workbench/videoPolling", route55); + app.use("/api/project/addProject", route56); + app.use("/api/project/delProject", route57); + app.use("/api/project/editProject", route58); + app.use("/api/project/getProject", route59); + app.use("/api/script/addScript", route60); + app.use("/api/script/delScript", route61); + app.use("/api/script/exportScript", route62); + app.use("/api/script/getScrptApi", route63); + app.use("/api/script/updateScript", route64); + app.use("/api/scriptAgent/getPlanData", route65); + app.use("/api/scriptAgent/setPlanData", route66); + app.use("/api/setting/agentDeploy/agentSetKey", route67); + app.use("/api/setting/agentDeploy/deployAgentModel", route68); + app.use("/api/setting/agentDeploy/getAgentDeploy", route69); + app.use("/api/setting/dbConfig/clearData", route70); + app.use("/api/setting/fileManagement/openFolder", route71); + app.use("/api/setting/getTextModel", route72); + app.use("/api/setting/loginConfig/getUser", route73); + app.use("/api/setting/loginConfig/updateUserPwd", route74); + app.use("/api/setting/memoryConfig/getMemory", route75); + app.use("/api/setting/memoryConfig/sureMemory", route76); + app.use("/api/setting/vendorConfig/addVendor", route77); + app.use("/api/setting/vendorConfig/deleteVendor", route78); + app.use("/api/setting/vendorConfig/getVendorList", route79); + app.use("/api/setting/vendorConfig/modelTest", route80); + app.use("/api/setting/vendorConfig/updateVendor", route81); + app.use("/api/task/getTaskApi", route82); + app.use("/api/task/getTaskCategories", route83); + app.use("/api/task/taskDetails", route84); + app.use("/api/test/test", route85); } diff --git a/src/routes/production/getFlowData.ts b/src/routes/production/getFlowData.ts index bd8733b..8110f71 100644 --- a/src/routes/production/getFlowData.ts +++ b/src/routes/production/getFlowData.ts @@ -154,31 +154,52 @@ export default router.post( }; }), ); - // 将原有 flowData.storyboard 按 id 建立索引,以便后续合并保留旧字段 - const existingStoryboardMap: Record = {}; + // 将数据库 storyboardData 按 id 建立索引 + const dbStoryboardMap: Record = {}; + storyboardData.forEach((i) => { + dbStoryboardMap[i.id!] = i; + }); + + // 用于构造单条 storyboard 的辅助函数 + const buildStoryboardItem = (i: (typeof storyboardData)[number], existing: any = {}) => ({ + ...existing, + id: i.id, + title: i.title, + description: i.description, + camera: i.camera, + duration: i.duration ? +i.duration : 0, + frameMode: i.frameMode, + prompt: i.prompt, + lines: i.lines, + sound: i.sound, + associateAssetsIds: assets2StoryboardMap[i.id!] ?? [], + src: i.filePath, + state: i.state, + }); + + // 保持旧数据顺序,新增的追加到最后 + const usedIds = new Set(); + const orderedStoryboard: any[] = []; + + // 1. 按旧数据顺序遍历,若数据库中仍存在则合并更新 if (Array.isArray(flowData.storyboard)) { flowData.storyboard.forEach((s: any) => { - existingStoryboardMap[s.id] = s; + const dbItem = dbStoryboardMap[s.id]; + if (dbItem) { + orderedStoryboard.push(buildStoryboardItem(dbItem, s)); + usedIds.add(s.id); + } }); } - 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, - }; + + // 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(400).send(error()); diff --git a/src/routes/production/storyboard/previewImage.ts b/src/routes/production/storyboard/previewImage.ts new file mode 100644 index 0000000..84e0611 --- /dev/null +++ b/src/routes/production/storyboard/previewImage.ts @@ -0,0 +1,115 @@ +import express from "express"; +import u from "@/utils"; +import { z } from "zod"; +import sharp from "sharp"; +import { success } from "@/lib/responseFormat"; +import { validateFields } from "@/middleware/middleware"; +const router = express.Router(); + +export default router.post( + "/", + validateFields({ + storyboardIds: z.array(z.number()), + }), + async (req, res) => { + const { storyboardIds } = req.body; + const storyboardImage = await u.db("o_storyboard").whereIn("id", storyboardIds).select("id", "filePath"); + + // 按 storyboardIds 顺序构建 filePath 映射 + const filePathMap: Record = {}; + storyboardImage.forEach((i) => { + filePathMap[i.id!] = i.filePath || ""; + }); + const orderedFilePaths = storyboardIds.map((id: number) => filePathMap[id]); + + // 读取所有图片 buffer 并获取元数据 + const loaded = await Promise.all( + orderedFilePaths.map(async (filePath: string) => { + if (!filePath) return null; + const buffer = await u.oss.getFile(filePath); + const metadata = await sharp(buffer).metadata(); + return { buffer, width: metadata.width || 0, height: metadata.height || 0 }; + }), + ); + + // 过滤掉无效图片 + const validImages = loaded.filter((img): img is NonNullable => img !== null && img.width > 0 && img.height > 0); + if (validImages.length === 0) { + return res.status(200).send(success(null)); + } + + // 计算网格布局 + const cols = Math.min(5, validImages.length); + const rows = Math.ceil(validImages.length / cols); + + const colWidths: number[] = Array(cols).fill(0); + const rowHeights: number[] = Array(rows).fill(0); + validImages.forEach((img, idx) => { + const c = idx % cols; + const r = Math.floor(idx / cols); + colWidths[c] = Math.max(colWidths[c], img.width); + rowHeights[r] = Math.max(rowHeights[r], img.height); + }); + + const canvasWidth = colWidths.reduce((a, b) => a + b, 0); + const canvasHeight = rowHeights.reduce((a, b) => a + b, 0); + + // 为每张图片生成带标号的合成层 + const compositeInputs: sharp.OverlayOptions[] = []; + + for (let i = 0; i < validImages.length; i++) { + const img = validImages[i]; + const c = i % cols; + const r = Math.floor(i / cols); + const x = colWidths.slice(0, c).reduce((a, b) => a + b, 0); + const y = rowHeights.slice(0, r).reduce((a, b) => a + b, 0); + + // 添加图片层 + compositeInputs.push({ + input: img.buffer, + left: x, + top: y, + }); + + // 生成标号标签 SVG + const label = `S${String(i + 1).padStart(2, "0")}`; + const fontSize = Math.max(14, Math.min(img.width, img.height) * 0.06); + const padding = Math.round(fontSize * 0.4); + // 估算文字宽度(等宽近似) + const textWidth = Math.round(label.length * fontSize * 0.65); + const bgW = textWidth + padding * 2; + const bgH = Math.round(fontSize) + padding * 2; + + const labelSvg = Buffer.from( + ` + + ${label} + `, + ); + + compositeInputs.push({ + input: labelSvg, + left: x + 4, + top: y + 4, + }); + } + + // 使用 sharp 创建画布并合成 + const resultBuffer = await sharp({ + create: { + width: canvasWidth, + height: canvasHeight, + channels: 4, + background: { r: 255, g: 255, b: 255, alpha: 1 }, + }, + }) + .composite(compositeInputs) + .png() + .toBuffer(); + + const base64 = resultBuffer.toString("base64"); + const dataUrl = `data:image/png;base64,${base64}`; + + return res.status(200).send(success(dataUrl)); + }, +);