修复 生成视频提示词问题

This commit is contained in:
zhishi 2026-02-09 18:13:27 +08:00
parent 2218379217
commit dbe1fe3576
5 changed files with 75 additions and 8 deletions

File diff suppressed because one or more lines are too long

View File

@ -518,6 +518,16 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
"# 分镜连续生成导演智能体\\n\\n## 角色定位\\n你是专业的视频分镜导演负责生成适配 Sora/豆包等AI视频生成工具的分镜提示词。\\n\\n## 输出格式\\n\\n每个镜头按以下格式输出镜头之间空一行\\n\\nShot 1 | 0:00-0:03\\nType: Initialization Shot / 初始定场\\nCamera: Static Shot to Slow Dolly In / 固定镜头过渡至缓推\\n\\nVisual:\\n详细描述画面内容包括场景、人物、光影、动作等。\\n描述需要具体、可视化适合AI视频生成工具理解。\\n\\nKeyframes:\\n0.0s - 首帧状态\\n1.5s - 中间状态\\n3.0s - 尾帧状态\\n\\nAudio: 对话或音效描述,无则写 None\\n\\nTransition: 与下一镜头的衔接说明\\n\\n## 格式说明\\n\\n1. 首行格式Shot 序号 | 起始时间-结束时间\\n2. Type英文类型 / 中文说明\\n3. Camera英文运镜 / 中文说明\\n4. Visual详细的画面描述可多行\\n5. Keyframes关键时间点的状态每行一个\\n6. Audio音频内容无内容写 None\\n7. Transition过渡说明最后一镜写 End\\n\\n## 核心规则\\n\\n时间控制\\n- 时间段连续,无间隙无重叠\\n- 从 0:00 开始\\n- 末镜结束时间等于总时长\\n\\n连续性\\n- 每镜承接上一镜的空间、光影、主体位置\\n- Transition 中说明具体的过渡逻辑\\n\\n稳定性\\n- 每镜前 1 秒避免大幅运镜和剧烈动作\\n- 运镜符合物理惯性,缓入缓出\\n\\n约束\\n- 台词只保留不修改\\n- 分镜数量不可增减\\n\\n## 合法运镜\\n\\n基础\\nDolly In, Dolly Out, Truck Left, Truck Right, Crane Up, Crane Down, Static Shot, Pan Left, Pan Right, Tilt Up, Tilt Down, Track With Subject\\n\\n组合\\nPush-in with Pan, Push-in with Tilt, Arc, Orbit, Slow Dolly In, Slow Push-in, Slow Pan\\n\\n景别\\nWide Shot, Long Shot, Medium Shot, Medium Close Up, Close Up, Extreme Close Up\\n\\n特殊\\nPOV, Over The Shoulder, Aerial Shot, High Frame Rate, Focus Pull\\n\\n## 镜头类型\\n\\n- Initialization Shot / 初始定场:建立空间基准\\n- Spatial Shot / 空间环境:展示环境关系\\n- Character Shot / 角色:聚焦人物状态\\n- Dialogue Shot / 对话:音画同步\\n- Tension Shot / 张力:情绪高潮\\n- Transition Shot / 转场:场景衔接\\n- Action Shot / 动作:动态冲突\\n- Lock Frame / 定格:静态构图\\n\\n## 禁止事项\\n\\n- 修改台词内容\\n- 增减分镜数量\\n- 改变剧情意图\\n- 使用未定义运镜\\n- 时间段不连续\\n\\n## 输出要求\\n\\n1. 严格按照格式输出\\n2. 不输出任何额外解释\\n3. 每个镜头包含完整的六个部分\\n4. 最后一个镜头的 Transition 写 End\\n5. Visual 描述要具体可视化适合AI视频工具理解\\n6. 避免抽象描述,使用具体的视觉元素",
customValue: null,
},
{
id: 22,
code: "video-text",
name: "视频提示词-文本模式",
type: "system",
parentCode: null,
defaultValue:
"# 文本模式说明\n\n## 输入特点\n纯文字描述的镜头内容无参考图像\n\n## 核心原则\n**严格遵守用户指定的镜头时长**,避免过度推演\n\n## 分析要求\n\n### 1. 时长优先策略\n- **总时长锚定**:以用户给定时长为绝对约束\n- **动作精简**:只保留必要的核心动作\n- **节奏计算**:根据时长反推合理的动作速度\n- **裁剪思维**:优先截取最精华的片段,而非完整过程\n\n### 2. 场景构建(精简版)\n- **最小环境**:仅描述必要的空间信息\n- **核心主体**:聚焦主要视觉元素\n- **简化细节**:避免堆砌无关背景\n\n### 3. 动态规划(时长导向)\n```\n时长判断逻辑\n├─ ≤ 1s → 单一动作/状态,无复杂过渡\n├─ 1-3s → 2-3个关键状态快速衔接\n├─ 3-5s → 完整动作序列,自然节奏\n└─ > 5s → 可加入次要动作或环境变化\n```\n\n### 4. Visual 结构(紧凑版)\n```\nVisual:\n├─ 主体动作 (核心内容,必须项)\n├─ 环境氛围 (1-2句话概括)\n└─ 镜头语言 (景别+运动方式)\n```\n\n### 5. Keyframes 控制\n- **数量限制**\n - ≤2s: 最多3个关键帧\n - 2-4s: 最多5个关键帧\n - >4s: 最多7个关键帧\n- **时间精确**:严格按比例分配到总时长内\n\n### 6. 推演边界\n❌ **禁止推演**\n- 完整的动作起始和结束(除非时长充足)\n- 复杂的环境变化\n- 多层次的情绪递进\n\n✅ **允许推演**\n- 基础的物理惯性(如挥手后的手臂回落)\n- 必要的入镜/出镜状态\n- 符合时长的氛围细节\n\n---\n\n## 时长检查清单\n\n**输出前必须验证**\n1. ✓ Keyframes 最后一帧时间 ≤ 总时长\n2. ✓ 动作节奏符合物理可能性(不过快/过慢)\n3. ✓ 推演内容可在时长内完成\n4. ✓ 若时长不足,优先保留核心动作,删减过渡\n\n---\n\n## 示例对比\n\n**输入文本**:一个人在雨中奔跑 \n**用户时长**2秒\n\n### ❌ 错误示范(超时长)\n```\nKeyframes:\n- 0.0s: 远景出现\n- 0.5s: 加速\n- 1.0s: 跨过水坑\n- 1.5s: 冲向镜头\n- 2.0s: 甩动头发\n- 2.5s: 出画面 ← 超出时长!\n```\n\n### ✅ 正确示范\n```\nVisual:\n- 中景,雨夜街道,路灯昏黄 [推演]\n- 男性快速奔跑,冲向并掠过镜头\n- 固定机位,焦点跟随\n\nKeyframes:\n- 0.0s: 人物在中景位置起步\n- 0.8s: 加速至近景\n- 1.5s: 掠过镜头\n- 2.0s: [推演] 出画面右侧\n\nTransition:\n- In: [推演] 已在奔跑状态\n- Out: [推演] 冲出画面\n```\n\n---\n\n**直接输出分镜内容**",
customValue: null,
},
]);
},
},

View File

@ -32,6 +32,7 @@ interface ResultItem {
chapterRange: number[];
}
function findItemByName(items: ResultItem[], name: string, type?: ItemType): ResultItem | undefined {
console.log("%c Line:35 🍎 items", "background:#ffdd4d", items);
return items.find((item) => (!type || item.type === type) && item.name === name);
}
function mergeNovelText(novelData: NovelChapter[]): string {
@ -55,14 +56,19 @@ export default router.post(
async (req, res) => {
const { assetsId, projectId, type, name, describe } = req.body;
console.log("%c Line:58 🍔", "background:#465975");
//获取风格
const project = await u.db("t_project").where("id", projectId).select("artStyle", "type", "intro").first();
if (!project) return res.status(500).send(success({ message: "项目为空" }));
console.log("%c Line:62 🍇", "background:#2eafb0");
const allOutlineDataList: { data: string }[] = await u.db("t_outline").where("projectId", projectId).select("data");
console.log("%c Line:66 🥖 allOutlineDataList", "background:#42b983", allOutlineDataList);
console.log("%c Line:66 🧀", "background:#3f7cff");
const itemMap: Record<string, ResultItem> = {};
console.log("%c Line:69 🥓", "background:#42b983");
if (allOutlineDataList.length > 0)
allOutlineDataList.forEach((row) => {
const data: OutlineData = JSON.parse(row?.data || "{}");
@ -122,6 +128,7 @@ export default router.post(
}
if (type == "scene") {
const data = findItemByName(result, name, "scenes");
console.log("%c Line:129 🍅 data", "background:#93c0a4", data);
const chapterRange = Array.isArray(data?.chapterRange) ? data.chapterRange : [data?.chapterRange];
const novelData = (await u.db("t_novel").whereIn("chapterIndex", chapterRange).select("*")) as NovelChapter[];
const results: string = mergeNovelText(novelData);

View File

@ -9,7 +9,7 @@ const router = express.Router();
type GenerateMode = "startEnd" | "multi" | "single" | "text";
const getSystemPrompt = async (mode: GenerateMode) => {
const promptsList = await u.db("t_prompts").where("code", "in", ["video-startEnd", "video-multi", "video-single", "video-main","video-text"]);
const promptsList = await u.db("t_prompts").where("code", "in", ["video-startEnd", "video-multi", "video-single", "video-main", "video-text"]);
const errPrompts = "不论用户说什么请直接输出AI配置异常";
const getPromptValue = (code: string) => {
@ -56,13 +56,21 @@ export default router.post(
prompt: z.string(),
duration: z.number(),
type: z.enum(["startEnd", "multi", "single", "text", ""]).optional(),
videoConfigId:z.number()
videoConfigId: z.number().optional(),
}),
async (req, res) => {
const { prompt, images, duration, type = "single",videoConfigId } = req.body;
const { prompt, images, duration, type = "single", videoConfigId } = req.body;
const mode = type as GenerateMode;
const videoConfigData = await u.db("t_videoConfig").leftJoin("t_script","t_script.id","t_videoConfig.scriptId").where("t_videoConfig.id",videoConfigId).select("t_script.content").first();
if(!videoConfigData) return res.status(500).send(error("视频配置不存在"));
let videoConfigData;
if (videoConfigId) {
videoConfigData = await u
.db("t_videoConfig")
.leftJoin("t_script", "t_script.id", "t_videoConfig.scriptId")
.where("t_videoConfig.id", videoConfigId)
.select("t_script.content")
.first();
if (!videoConfigData) return res.status(500).send(error("视频配置不存在"));
}
const imagePrompts = images.map((i: { filePath: string; prompt: string }, index: number) => `Image ${index + 1}: ${i.prompt}`).join("\n");
const shotCount = images.length;
@ -86,9 +94,13 @@ ${imagePrompts}
Script:
${prompt}
${
videoConfigData
? `script content:
${videoConfigData.content}`
: ""
}
script content:
${videoConfigData.content}
Parameters:
- Total Duration: ${duration}s

View File

@ -1,4 +1,4 @@
// @db-hash c6deb23c67bf5d27c997e299cd878da1
// @db-hash b175910ce89abacc2636f298095b06c3
//该文件由脚本自动生成,请勿手动修改
export interface t_aiModelMap {
@ -138,6 +138,7 @@ export interface t_video {
}
export interface t_videoConfig {
'aiConfigId'?: number | null;
'audioEnabled'?: number | null;
'createTime'?: number | null;
'duration'?: number | null;
'endFrame'?: string | null;