video-flow-toon/src/routes/storyboard/generateVideoPrompt.ts
ACT丶流星雨 eb8eb94b57 no message
2026-02-09 16:41:13 +08:00

211 lines
6.6 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 { error, success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { z } from "zod";
import axios from "axios";
const router = express.Router();
const prompt = `
你是一名资深动画导演,擅长将静态分镜转化为简洁、专业、详尽的 Motion Prompt视频生成动作提示。你理解镜头语言、情绪节奏能补充丰富但不重复静态元素只突出变化与动态。
## 任务
你将接收用户输入的:
- **分镜图片**(单张)
- **分镜提示词**(对应该镜头)
- **剧本内容**
你需输出**规范的 Motion Prompt JSON 对象**。
---
## 核心要求
### 1. 画面类型描述(必需,开头一句)
- 明确本分镜属于:**前景/近景/中景/远景/全景**
- 表述格式:"中景。" / "近景。" / "远景。" / "全景。"
### 3. 细致动作叙述
清晰分别描述以下要素:
- **镜头运动**1种5-20字推拉摇移、跟随、固定等
- **角色核心动作**1-2种20-60字主体动作+情绪细节
- **环境动态**0-1种10-30字光影、物体、自然元素变化
- **速度节奏**5-15字缓慢、急促、平稳等
- **氛围风格**可选10-20字情绪渲染、视觉基调
用"" "并且" "同时"等词串联,使句子流畅连贯。
### 4. 长度优化
- **content 必须在 80-150 字之间**
- 若不足 80 字,补充:
- 角色细微神态(眼神、呼吸、肌肉紧张度)
- 动作过渡细节(转身、停顿、重心转移)
- 环境反应(光影变化、物体晃动)
- **禁止引入图片中已有的静态描述**
---
## 结构推荐
**标准结构:**
画面类型。镜头运动,角色主动作+情绪表现+微动作细节,环境动态(如有),速度节奏,氛围渲染。
**参考示例:**
- 中景。镜头缓慢推进,角色身体微微紧绷,神情凝重,缓缓转头注视门口,眉头微皱、唇角轻颤,光影在脸上拉出一缕阴影,衣角随动作轻晃,气氛变得紧张。
- 远景。镜头稳定,角色站立不动,但指尖不停地敲打桌面,目光游移不定,窗外树影摇曳,光线逐渐变暗,整体节奏平稳,渲染出迟疑与不安。
---
## 禁忌
❌ 不重复任何静态画面元素(外观、场景、服装、道具等)
❌ 不使用否定句、抽象形容词
❌ 不超过 2 种主体动作、1 种镜头运动、1 种环境动态
❌ 不分多场景,单个 content 不超过 200 字
---
## 输出格式
返回 **JSON 对象**,包含:
{
"time": 数字1-15镜头时长秒数,
"name": "字符串2-6字概括镜头动态/情绪)",
"content": "字符串80-150字首句为画面类型充分描述动态细节"
}
### 字段说明
- **time**:根据动作复杂度合理分配,简单动作 2-5 秒,复杂动作 6-10 秒
- **name**:精炼概括本镜头核心动态或情绪转折
- **content**:首句必须是画面类型,后续流畅衔接动态描述
---
## 处理流程
1. **分析输入的单张图片**
2. **生成对应的 JSON 对象**
3. **检查 content 字段:**
- 首句是否为画面类型
- 字数是否在 80-150 之间
- 是否避免了静态描述
---
现在请根据我提供的分镜内容,严格按照以上规则输出 Motion Prompt JSON 对象。
`;
async function urlToBase64(imageUrl: string): Promise<string> {
const response = await axios.get(imageUrl, { responseType: "arraybuffer" });
const contentType = response.headers["content-type"] || "image/png";
const base64 = Buffer.from(response.data, "binary").toString("base64");
return `data:${contentType};base64,${base64}`;
}
// 生成单个分镜提示
async function generateSingleVideoPrompt({
scriptText,
storyboardPrompt,
ossPath,
}: {
scriptText: string;
storyboardPrompt: string;
ossPath: string;
}): Promise<{ content: string; time: number; name: string }> {
const messages: any[] = [
{
role: "system",
content: prompt,
},
{
role: "user",
content: [
{
type: "text",
text: `剧本内容:${scriptText}\n分镜提示词:${storyboardPrompt}`,
},
{
type: "image",
image: await urlToBase64(ossPath),
},
],
},
];
try {
const apiConfig = await u.getPromptAi("videoPrompt");
const result = await u.ai.text.invoke(
{
messages,
output: {
time: z.number().describe("时长,镜头时长 1-15"),
content: z.string().describe("提示词内容"),
name: z.string().describe("分镜名称"),
},
},
apiConfig,
);
if (!result) {
console.error("AI 返回结果为空:", result);
throw new Error("AI 返回结果为空");
}
if (!result.content || result.time === undefined || !result.name) {
console.error("AI 返回格式错误:", result);
throw new Error("AI 返回格式错误");
}
return result;
} catch (err: any) {
console.error("generateSingleVideoPrompt 调用失败:", err?.message || err);
throw new Error(`生成视频提示词失败: ${err?.message || "未知错误"}`);
}
}
// 主路由 - 单张图片处理
export default router.post(
"/",
validateFields({
projectId: z.number(),
scriptId: z.number().nullable(),
id: z.string(),
prompt: z.string().optional(),
src: z.string(),
}),
async (req, res) => {
const { projectId, scriptId, id, prompt: imagePrompt, src } = req.body;
try {
const scriptData = await u.db("t_script").where("id", scriptId).select("content").first();
if (!scriptData) return res.status(500).send(error("剧本不存在"));
const projectData = await u.db("t_project").where({ id: +projectId }).select("artStyle", "videoRatio").first();
if (!projectData) return res.status(500).send(error("项目不存在"));
const result = await generateSingleVideoPrompt({
scriptText: scriptData.content!,
storyboardPrompt: imagePrompt || "",
ossPath: src,
});
res.status(200).send(
success({
id,
videoPrompt: result.content || "",
prompt: imagePrompt,
duration: String(result.time || ""),
projectId,
type: "分镜",
name: result.name || "",
scriptId,
src,
}),
);
} catch (err: any) {
console.error("生成视频提示词失败:", err?.message || err);
res.status(500).send(error(err?.message || "生成视频提示词失败"));
}
},
);