diff --git a/src/agents/productionAgent/tools.ts b/src/agents/productionAgent/tools.ts index a4f57f6..169cb25 100644 --- a/src/agents/productionAgent/tools.ts +++ b/src/agents/productionAgent/tools.ts @@ -284,6 +284,7 @@ export default (resTool: ResTool, toolsNames?: string[]) => { lines: item.lines, state: "未生成", scriptId: resTool.data.scriptId, + createTime: Date.now(), }); if (item.associateAssetsIds.length) { await u.db("o_assets2Storyboard").insert(item.associateAssetsIds.map((i) => ({ storyboardId: insertedId, assetId: i }))); diff --git a/src/router.ts b/src/router.ts index 291c400..f1a88b4 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,4 +1,4 @@ -// @routes-hash f5f78866e59979bf30af031c9ea0de82 +// @routes-hash ced882fe9cc49f6e16ade49cf276b583 import { Express } from "express"; import route1 from "./routes/agents/clearMemory"; @@ -49,43 +49,45 @@ 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/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"; +import route49 from "./routes/production/storyboard/downPreviewImage"; +import route50 from "./routes/production/storyboard/getStoryboardData"; +import route51 from "./routes/production/storyboard/previewImage"; +import route52 from "./routes/production/workbench/confirmSelection"; +import route53 from "./routes/production/workbench/delVideo"; +import route54 from "./routes/production/workbench/generateVideo"; +import route55 from "./routes/production/workbench/getChatLines"; +import route56 from "./routes/production/workbench/getVideoModelDetail"; +import route57 from "./routes/production/workbench/videoPolling"; +import route58 from "./routes/project/addProject"; +import route59 from "./routes/project/delProject"; +import route60 from "./routes/project/editProject"; +import route61 from "./routes/project/getProject"; +import route62 from "./routes/script/addScript"; +import route63 from "./routes/script/delScript"; +import route64 from "./routes/script/exportScript"; +import route65 from "./routes/script/getScrptApi"; +import route66 from "./routes/script/updateScript"; +import route67 from "./routes/scriptAgent/getPlanData"; +import route68 from "./routes/scriptAgent/setPlanData"; +import route69 from "./routes/setting/agentDeploy/agentSetKey"; +import route70 from "./routes/setting/agentDeploy/deployAgentModel"; +import route71 from "./routes/setting/agentDeploy/getAgentDeploy"; +import route72 from "./routes/setting/dbConfig/clearData"; +import route73 from "./routes/setting/fileManagement/openFolder"; +import route74 from "./routes/setting/getTextModel"; +import route75 from "./routes/setting/loginConfig/getUser"; +import route76 from "./routes/setting/loginConfig/updateUserPwd"; +import route77 from "./routes/setting/memoryConfig/getMemory"; +import route78 from "./routes/setting/memoryConfig/sureMemory"; +import route79 from "./routes/setting/vendorConfig/addVendor"; +import route80 from "./routes/setting/vendorConfig/deleteVendor"; +import route81 from "./routes/setting/vendorConfig/getVendorList"; +import route82 from "./routes/setting/vendorConfig/modelTest"; +import route83 from "./routes/setting/vendorConfig/updateVendor"; +import route84 from "./routes/task/getTaskApi"; +import route85 from "./routes/task/getTaskCategories"; +import route86 from "./routes/task/taskDetails"; +import route87 from "./routes/test/test"; export default async (app: Express) => { app.use("/api/agents/clearMemory", route1); @@ -136,41 +138,43 @@ 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/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); + app.use("/api/production/storyboard/downPreviewImage", route49); + app.use("/api/production/storyboard/getStoryboardData", route50); + app.use("/api/production/storyboard/previewImage", route51); + app.use("/api/production/workbench/confirmSelection", route52); + app.use("/api/production/workbench/delVideo", route53); + app.use("/api/production/workbench/generateVideo", route54); + app.use("/api/production/workbench/getChatLines", route55); + app.use("/api/production/workbench/getVideoModelDetail", route56); + app.use("/api/production/workbench/videoPolling", route57); + app.use("/api/project/addProject", route58); + app.use("/api/project/delProject", route59); + app.use("/api/project/editProject", route60); + app.use("/api/project/getProject", route61); + app.use("/api/script/addScript", route62); + app.use("/api/script/delScript", route63); + app.use("/api/script/exportScript", route64); + app.use("/api/script/getScrptApi", route65); + app.use("/api/script/updateScript", route66); + app.use("/api/scriptAgent/getPlanData", route67); + app.use("/api/scriptAgent/setPlanData", route68); + app.use("/api/setting/agentDeploy/agentSetKey", route69); + app.use("/api/setting/agentDeploy/deployAgentModel", route70); + app.use("/api/setting/agentDeploy/getAgentDeploy", route71); + app.use("/api/setting/dbConfig/clearData", route72); + app.use("/api/setting/fileManagement/openFolder", route73); + app.use("/api/setting/getTextModel", route74); + app.use("/api/setting/loginConfig/getUser", route75); + app.use("/api/setting/loginConfig/updateUserPwd", route76); + app.use("/api/setting/memoryConfig/getMemory", route77); + app.use("/api/setting/memoryConfig/sureMemory", route78); + app.use("/api/setting/vendorConfig/addVendor", route79); + app.use("/api/setting/vendorConfig/deleteVendor", route80); + app.use("/api/setting/vendorConfig/getVendorList", route81); + app.use("/api/setting/vendorConfig/modelTest", route82); + app.use("/api/setting/vendorConfig/updateVendor", route83); + app.use("/api/task/getTaskApi", route84); + app.use("/api/task/getTaskCategories", route85); + app.use("/api/task/taskDetails", route86); + app.use("/api/test/test", route87); } diff --git a/src/routes/production/storyboard/downPreviewImage.ts b/src/routes/production/storyboard/downPreviewImage.ts new file mode 100644 index 0000000..fc35b74 --- /dev/null +++ b/src/routes/production/storyboard/downPreviewImage.ts @@ -0,0 +1,116 @@ +import express from "express"; +import u from "@/utils"; +import { z } from "zod"; +import sharp from "sharp"; +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 并获取元数据 + // sharp 底层 libvips 在 composite 时会将所有 input 解码为内存像素再合成,无需预转格式 + 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) { + res.status(204).end(); + return; + } + + // 计算网格布局 + 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 创建画布并合成,输出 PNG 无损格式避免压缩损耗 + const resultBuffer = await sharp({ + create: { + width: canvasWidth, + height: canvasHeight, + channels: 4, + background: { r: 255, g: 255, b: 255, alpha: 1 }, + }, + }) + .composite(compositeInputs) + .png({ compressionLevel: 3 }) + .toBuffer(); + + // 以文件下载形式返回 + res.setHeader("Content-Type", "image/png"); + res.setHeader("Content-Disposition", "attachment; filename=storyboard-preview.png"); + res.setHeader("Content-Length", resultBuffer.length); + res.status(200).send(resultBuffer); + }, +); diff --git a/src/routes/production/storyboard/getStoryboardData.ts b/src/routes/production/storyboard/getStoryboardData.ts new file mode 100644 index 0000000..6a90c68 --- /dev/null +++ b/src/routes/production/storyboard/getStoryboardData.ts @@ -0,0 +1,58 @@ +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({ + scriptId: z.number(), + page: z.number(), + limit: z.number(), + name: z.string().optional().nullable(), + }), + async (req, res) => { + const { scriptId, page, limit, name } = req.body; + const offset = (page - 1) * limit; + + const storyboardData = await u + .db("o_storyboard") + .where({ scriptId }) + .modify((qb) => { + if (name) { + qb.andWhere("title", "like", `%${name}%`); + } + }) + .offset(offset) + .limit(limit); + const data = await Promise.all( + storyboardData.map(async (i: any) => { + return { + id: i.id, + title: i.title, + prompt: i.prompt, + description: i.description, + camera: i.camera, + lines: i.lines, + sound: i.sound, + state: i.state, + src: i.filePath ? await u.oss.getFileUrl(i.filePath!) : "", + }; + }), + ); + const totalQuery = (await u + .db("o_storyboard") + .where({ scriptId }) + .modify((qb) => { + if (name) { + qb.andWhere("title", "like", `%${name}%`); + } + }) + .count("* as total") + .first()) as any; + + res.status(200).send(success({ data: data, total: totalQuery?.total })); + }, +); diff --git a/src/routes/production/storyboard/previewImage.ts b/src/routes/production/storyboard/previewImage.ts index 84e0611..e373f81 100644 --- a/src/routes/production/storyboard/previewImage.ts +++ b/src/routes/production/storyboard/previewImage.ts @@ -38,13 +38,28 @@ export default router.post( return res.status(200).send(success(null)); } + // 将每张图片缩放到合理尺寸,单张最大宽度 512px + const maxThumbWidth = 512; + const resizedImages = await Promise.all( + validImages.map(async (img) => { + if (img.width <= maxThumbWidth) { + return img; + } + const scale = maxThumbWidth / img.width; + const newWidth = maxThumbWidth; + const newHeight = Math.round(img.height * scale); + const buffer = await sharp(img.buffer).resize(newWidth, newHeight).toBuffer(); + return { buffer, width: newWidth, height: newHeight }; + }), + ); + // 计算网格布局 - const cols = Math.min(5, validImages.length); - const rows = Math.ceil(validImages.length / cols); + const cols = Math.min(5, resizedImages.length); + const rows = Math.ceil(resizedImages.length / cols); const colWidths: number[] = Array(cols).fill(0); const rowHeights: number[] = Array(rows).fill(0); - validImages.forEach((img, idx) => { + resizedImages.forEach((img, idx) => { const c = idx % cols; const r = Math.floor(idx / cols); colWidths[c] = Math.max(colWidths[c], img.width); @@ -57,8 +72,8 @@ export default router.post( // 为每张图片生成带标号的合成层 const compositeInputs: sharp.OverlayOptions[] = []; - for (let i = 0; i < validImages.length; i++) { - const img = validImages[i]; + for (let i = 0; i < resizedImages.length; i++) { + const img = resizedImages[i]; const c = i % cols; const r = Math.floor(i / cols); const x = colWidths.slice(0, c).reduce((a, b) => a + b, 0); @@ -104,11 +119,11 @@ export default router.post( }, }) .composite(compositeInputs) - .png() + .jpeg({ quality: 80 }) .toBuffer(); const base64 = resultBuffer.toString("base64"); - const dataUrl = `data:image/png;base64,${base64}`; + const dataUrl = `data:image/jpeg;base64,${base64}`; return res.status(200).send(success(dataUrl)); }, diff --git a/src/types/database.d.ts b/src/types/database.d.ts index 6aef2aa..85e26c3 100644 --- a/src/types/database.d.ts +++ b/src/types/database.d.ts @@ -1,6 +1,25 @@ -// @db-hash a3673cf3a1d1c9cbf22ae3cfff196a71 +// @db-hash 71b2e55243e59382321a140a8d9a64ff //该文件由脚本自动生成,请勿手动修改 +export interface _o_storyboard_old_20260325 { + 'camera'?: string | null; + 'createTime'?: number | null; + 'description'?: string | null; + 'duration'?: string | null; + 'filePath'?: string | null; + 'frameMode'?: string | null; + 'id'?: number; + 'lines'?: string | null; + 'mode'?: string | null; + 'model'?: string | null; + 'prompt'?: string | null; + 'reason'?: string | null; + 'resolution'?: string | null; + 'scriptId'?: number | null; + 'sound'?: string | null; + 'state'?: string | null; + 'title'?: string | null; +} export interface memories { 'content': string; 'createTime': number; @@ -138,6 +157,7 @@ export interface o_storyboard { 'filePath'?: string | null; 'frameMode'?: string | null; 'id'?: number; + 'index'?: string | null; 'lines'?: string | null; 'mode'?: string | null; 'model'?: string | null; @@ -201,6 +221,7 @@ export interface o_videoConfig { } export interface DB { + "_o_storyboard_old_20260325": _o_storyboard_old_20260325; "memories": memories; "o_agentDeploy": o_agentDeploy; "o_agentWorkData": o_agentWorkData;