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 { 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 || "生成视频提示词失败")); } }, );