video-flow-toon/src/routes/video/generateVideo.ts
2026-02-06 14:18:30 +08:00

176 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import express from "express";
import u from "@/utils";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 生成视频
export default router.post(
"/",
validateFields({
projectId: z.number(),
scriptId: z.number(),
configId: z.number().optional(), // 关联的视频配置ID
type: z.string().optional(),
resolution: z.string(),
filePath: z.array(z.string()),
duration: z.number(),
prompt: z.string(),
}),
async (req, res) => {
const { type, scriptId, projectId, configId, resolution, filePath, duration, prompt } = req.body;
// 参数校验
if (type === "volcengine") {
if (duration < 4 || duration > 12) {
return res.status(400).send(error("视频时长需在4-12秒之间"));
}
if (!["480p", "720p", "1080p"].includes(resolution)) {
return res.status(400).send(error("视频分辨率不正确"));
}
}
if (type === "runninghub") {
if (duration !== 10 && duration !== 15) {
return res.status(400).send(error("视频时长只能是10秒或15秒"));
}
if (resolution !== "9:16" && resolution !== "16:9") {
return res.status(400).send(error("视频分辨率不正确"));
}
}
// 过滤掉空值
let fileUrl = filePath.filter((p: string) => p && p.trim() !== "");
if (fileUrl.length === 0) {
return res.status(400).send(error("请至少选择一张图片"));
}
// 处理文件路径,如果是 base64 则上传到 OSS
if (fileUrl.length === 1) {
const match = fileUrl[0].match(/base64,([A-Za-z0-9+/=]+)/);
if (match && match.length >= 2) {
const imagePath = `/${projectId}/assets/${uuidv4()}.jpg`;
const buffer = Buffer.from(match[1], "base64");
await u.oss.writeFile(imagePath, buffer);
fileUrl = [await u.oss.getFileUrl(imagePath)];
}
}
// 提取路径名的辅助函数
const getPathname = (url: string): string => {
// 如果是完整 URL提取 pathname
if (url.startsWith("http://") || url.startsWith("https://")) {
return new URL(url).pathname;
}
// 否则认为已经是路径
return url;
};
// 校验文件是否存在
const fileExistsResults = await Promise.all(
fileUrl.map(async (url: string) => {
const path = getPathname(url);
return u.oss.fileExists(path);
}),
);
if (!fileExistsResults.every(Boolean)) {
return res.status(400).send(error("选择分镜文件不存在"));
}
const firstFrame = getPathname(fileUrl[0]);
const storyboardImgs = fileUrl.map((path: string) => getPathname(path));
const savePath = `/${projectId}/video/${uuidv4()}.mp4`;
// 先插入记录state 默认为 0
const [videoId] = await u.db("t_video").insert({
scriptId,
configId: configId || null, // 关联的视频配置ID
time: duration,
resolution,
prompt,
firstFrame,
storyboardImgs: JSON.stringify(storyboardImgs),
filePath: savePath,
state: 0,
});
// 立即返回,不等待视频生成
res.status(200).send(success({ id: videoId, configId: configId || null }));
// 异步生成视频
generateVideoAsync(videoId, projectId, fileUrl, savePath, prompt, duration, resolution, type);
},
);
// 异步生成视频
async function generateVideoAsync(
videoId: number,
projectId: number,
fileUrl: string[],
savePath: string,
prompt: string,
duration: number,
resolution: string,
type?: string,
) {
try {
const projectData = await u.db("t_project").where("id", projectId).select("artStyle").first();
// 提取路径名的辅助函数
const getPathname = (url: string): string => {
if (url.startsWith("http://") || url.startsWith("https://")) {
return new URL(url).pathname;
}
return url;
};
const imageBase64 = await Promise.all(
fileUrl.map((path: string) => {
if (path.startsWith("http://") || path.startsWith("https://")) {
return u.oss.getImageBase64(getPathname(path));
}
// 如果是相对路径,直接获取
return u.oss.getImageBase64(path);
}),
);
const inputPrompt = `
请完全参照以下内容生成视频:
${prompt}
重要强调:
风格高度保持${projectData?.artStyle || "CG"}风格,保证人物一致性
1. 视频整体风格、色调、光影、人脸五官与参考图片保持高度一致
2. 保证视频连贯性、前后无矛盾
3. 关键人物在画面中全部清晰显示,不得被遮挡、缺失或省略
4. 画面真实、细致无畸形、无模糊、无杂物、无多余人物、无文字、水印、logo
`;
const videoPath = await u.ai.video({
imageBase64,
savePath,
prompt: inputPrompt,
duration: duration as any,
aspectRatio: resolution as any,
resolution: resolution as any,
});
if (videoPath) {
// 生成成功,更新状态为 1
await u.db("t_video").where("id", videoId).update({
filePath: videoPath,
state: 1,
});
} else {
// 生成失败,更新状态为 -1
await u.db("t_video").where("id", videoId).update({ state: -1 });
}
} catch (err) {
console.error(`视频生成失败 videoId=${videoId}:`, err);
await u.db("t_video").where("id", videoId).update({ state: -1 });
}
}