This commit is contained in:
zhishi 2026-03-21 17:51:09 +08:00
commit 0d6f35726b
4 changed files with 173 additions and 56 deletions

View File

@ -307,6 +307,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
name: "o_storyboard",
builder: (table) => {
table.integer("id").notNullable();
table.integer("scriptId");
table.text("name");
table.text("detail");
table.text("prompt");
@ -334,11 +335,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
name: "o_video",
builder: (table) => {
table.integer("id").notNullable();
table.text("resolution");
table.text("prompt");
table.text("filePath");
table.text("model");
table.text("mode");
table.text("errorReason");
table.integer("time");
table.text("state");
@ -348,15 +345,15 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
table.unique(["id"]);
},
},
//视频配置
{
name: "o_videoConfig",
builder: (table) => {
table.integer("id").notNullable();
table.integer("videoId"); //视频Id
table.integer("audio"); //声音
table.text("model"); //模型
table.text("mode"); // 模式startEnd/multi/single
table.integer("storyboardId");
table.integer("videoId");
table.integer("audio"); // 声音
table.text("model"); // 模型
table.text("mode"); // 模式:
table.text("data"); // 所选数据集图片 JSON
table.text("resolution"); // 分辨率
table.integer("duration"); // 时长

View File

@ -4,15 +4,82 @@ import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 获取生产数据
export default router.post(
"/",
validateFields({
projectId: z.number(),
}),
async (req, res) => {
const { projectId } = req.body;
res.status(200).send(success("123"));
"/",
validateFields({
scriptId: z.number(),
}),
async (req, res) => {
const { scriptId } = req.body;
// 1. 查出该剧本下所有分镜
const storyboards = await u
.db("o_storyboard")
.where("o_storyboard.scriptId", scriptId)
.select(
"o_storyboard.id",
"o_storyboard.name",
"o_storyboard.detail",
"o_storyboard.prompt",
"o_storyboard.seconds",
"o_storyboard.filePath",
"o_storyboard.frameType",
"o_storyboard.scriptId",
)
.orderBy("o_storyboard.createTime", "asc");
if (storyboards.length === 0) {
return res.status(200).send(success([]));
}
const storyboardIds = storyboards.map((s) => s.id as number);
// 2. 批量查出所有相关视频
const videos = await u
.db("o_video")
.whereIn("o_video.storyboardId", storyboardIds)
.select("o_video.id", "o_video.storyboardId", "o_video.filePath", "o_video.state", "o_video.errorReason")
.orderBy("o_video.time", "desc");
// 3. 批量查出所有相关配置
const configs = await u
.db("o_videoConfig")
.whereIn("o_videoConfig.storyboardId", storyboardIds)
.select(
"o_videoConfig.id",
"o_videoConfig.storyboardId",
"o_videoConfig.videoId",
"o_videoConfig.prompt",
"o_videoConfig.model",
"o_videoConfig.mode",
"o_videoConfig.resolution",
"o_videoConfig.duration",
"o_videoConfig.audio",
"o_videoConfig.data",
);
// 4. 按 storyboardId 建立 Map 方便聚合
const videoMap = new Map<number, typeof videos>();
for (const video of videos) {
const sid = video.storyboardId as number;
if (!videoMap.has(sid)) videoMap.set(sid, []);
videoMap.get(sid)!.push(video);
}
const configMap = new Map(configs.map((c) => [c.storyboardId as number, c]));
// 5. 组装结果:分镜平铺 + config 对象 + videos 数组
const data = await Promise.all(
storyboards.map(async (storyboard) => {
const sid = storyboard.id as number;
return {
...storyboard,
filePath: storyboard.filePath && (await u.oss.getFileUrl(storyboard.filePath!)),
config: configMap.get(sid) ?? null,
videos: videoMap.get(sid) ?? [],
};
}),
);
return res.status(200).send(success(data));
},
);

View File

@ -13,15 +13,79 @@ export default router.post(
projectId: z.number(),
storyboardId: z.number(),
prompt: z.string(),
data: z.array(z.string()).optional(),
data: z
.array(
z.object({
id: z.number(),
type: z.string(),
}),
)
.optional(),
model: z.string(),
duration: z.number(),
duration: z.string(),
resolution: z.string(),
audio: z.boolean().optional(),
modeData: z.string(),
mode: z.string(),
}),
async (req, res) => {
const { scriptId, projectId, storyboardId, prompt, data, model, duration, resolution, audio, modeData } = req.body;
const { scriptId, projectId, storyboardId, prompt, data, model, duration, resolution, audio, mode } = req.body;
const videoPath = `/${projectId}/video/${uuidv4()}.mp4`; //视频保存路径
//新增
const [videoId] = await u.db("o_video").insert({
filePath: videoPath,
time: Date.now(),
state: "生成中",
scriptId,
storyboardId,
});
//查询分镜是否已有配置
const config = await u.db("o_videoConfig").where({ storyboardId }).first();
//保存配置
if (config) {
await u
.db("o_videoConfig")
.update({ audio, model, mode, data: JSON.stringify(data), resolution, duration, prompt, updateTime: Date.now() })
.where({ id: config.id });
} else {
await u.db("o_videoConfig").insert({
storyboardId,
audio,
model,
mode,
data: JSON.stringify(data),
resolution,
duration,
prompt,
createTime: Date.now(),
updateTime: Date.now(),
});
}
//查询出图片数据
const images = await Promise.all(
data.map(async (item: { id: number; type: string }) => {
if (item.type === "storyboard") {
const filePath = await u.db("o_storyboard").where("id", item.id).select("filePath").first();
return filePath?.filePath;
}
if (item.type === "assets") {
const filePath = await u
.db("o_assets")
.where("o_assets.id", item.id)
.leftJoin("o_image", "o_assets.imageId", "o_image.id")
.select("o_image.filePath")
.first();
return filePath?.filePath;
}
}),
);
//把images里面的图片转成base64格式
const base64 = await Promise.all(
images.map(async (item) => {
if (!item) return null;
return await u.oss.getImageBase64(item);
}),
);
//开始生成
try {
const relatedObjects = {
id: storyboardId,
@ -34,44 +98,33 @@ export default router.post(
3.
4.
5. `;
const videoPath = `/${projectId}/video/${uuidv4()}.mp4`;
const aiVideo = u.Ai.Video(model);
await aiVideo.run({
systemPrompt, // 系统提示词
projectId: projectId,
storyboardId: storyboardId,
prompt: prompt,
data: data,
modeData: modeData,
duration: duration,
resolution: resolution,
audio: audio,
systemPrompt,
projectId,
storyboardId,
prompt,
imageBase64: base64.filter((item) => item !== null) as string[],
mode,
duration,
resolution,
audio,
taskClass: "视频生成",
describe: "根据提示词生成视频",
relatedObjects: JSON.stringify(relatedObjects),
});
await aiVideo.save(videoPath); // 保存视频
//保存视频信息到数据库
// await u.db("o_video").insert({
// resolution,
// prompt,
// filePath: videoPath,
// model,
// time: Date.now(),
// state: "生成成功",
// scriptId: scriptId,
// });
res.status(200).send(success("视频生成成功"));
} catch (error) {
// await u.db("o_video").insert({
// resolution,
// prompt,
// model,
// time: Date.now(),
// state: "生成失败",
// scriptId: scriptId,
// errorReason: error instanceof Error ? error.message : "未知错误",
// });
await aiVideo.save(videoPath);
await u.db("o_video").where("id", videoId).update({ state: "生成成功" });
res.status(200).send(success({ videoId, message: "视频生成成功" }));
} catch (error: any) {
await u
.db("o_video")
.where("id", videoId)
.update({
state: "生成失败",
errorReason: error instanceof Error ? error.message : "未知错误",
});
res.status(500).send({ error: "视频生成失败" });
}
},

View File

@ -117,8 +117,8 @@ interface VideoConfig {
storyboardId: number; // 关联的分镜ID
systemPrompt?: string; // 系统提示词
prompt: string; //视频提示词
data: string[]; //输入的图片提示词
modeData: string; //模式
imageBase64: string[]; //输入的图片提示词
mode: string; //模式
duration: number; // 视频时长,单位秒
resolution: string; // 视频分辨率
audio: boolean; // 是否需要配音