prompts.mjs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. // lib/prompts.mjs — 提示词模板库(代码侧)
  2. //
  3. // 人读版见 references/prompt-templates.md;本文件是脚本直接调用的构建器。
  4. // 原则:人物一致性靠「固定外观锚点(appearance anchor)」逐镜头复述,
  5. // 空镜靠「场景锚点 + 运镜词」,避免 AI 每镜换脸/换景。
  6. /** 作曲填词的用户提示词 */
  7. export function buildLyricsPrompt({ theme, genre = '', mood = '', style = '', language = '中文', extra = '' }) {
  8. return `请为下面这个主题创作一首${language}流行歌曲的完整素材。
  9. 【主题】${theme}
  10. ${genre ? `【指定曲风】${genre}` : '【曲风】你来定,但需说明理由'}
  11. ${mood ? `【情绪基调】${mood}` : ''}
  12. ${style ? `【补充要求】${style}` : ''}
  13. ${extra ? `【其它】${extra}` : ''}
  14. 要求:
  15. - 歌词长度适合 2.5~3.5 分钟的歌曲,段落完整(主歌2段、副歌2-3次、可含桥段)。
  16. - 副歌必须有记忆点,句尾尽量押韵。
  17. - 歌词必须原创,不得使用任何已有歌曲的句子。
  18. - 给出英文 stylePrompt,供 AI 音乐模型直接使用(含曲风、乐器、速度、人声类型、混音质感)。`;
  19. }
  20. /** 分镜脚本的用户提示词(产线A:歌曲 MV) */
  21. export function buildStoryboardPrompt({ song, shotCount = 8, aspect = '16:9', perShot = 6, project = '' }) {
  22. const structure = (song.structure || []).join(' → ');
  23. return `为下面这首歌设计一支 AI 生成的音乐 MV 分镜(全程无实拍,纯 AI 生成画面)。
  24. 【歌名】${song.title}
  25. 【曲风/情绪】${song.genre || '-'} / ${song.mood || '-'} / ${song.bpm || '?'} BPM
  26. 【结构】${structure}
  27. 【画面比例】${aspect}
  28. 【镜头数量】${shotCount} 个(必须控制在 ${shotCount}±2 个,这是硬性约束)
  29. 【单镜时长】每镜 ${perShot} 秒左右,**必须在 ${Math.max(3, perShot - 2)}~${Math.min(8, perShot + 2)} 秒之间**。
  30. 注意:AI 图生视频单段现实上限约 8 秒,**不要**生成 20 秒、30 秒的镜头。
  31. 【总时长】因此全片约 ${shotCount * perShot} 秒;不要为了凑原曲长度而增加镜头数量或拉长单镜。
  32. 如需覆盖整首歌,请由使用者调大 --shots,而不是让单个镜头变长。
  33. ${project ? `【项目背景】${project}` : ''}
  34. 【歌词】
  35. ${song.lyrics}
  36. 设计要求:
  37. 1. 先定义「人物设定 appearanceAnchor」:一句话锁定主角外貌(性别/年龄/发型/服装/气质/镜头感),
  38. 之后每个含人物的镜头 description 与 imagePrompt 都必须原样复述这段锚点,保证跨镜头人物一致。
  39. 2. 镜头分为两类:
  40. - "broll":空镜/场景镜头(城市、天空、房间、物件特写…),无人物或人物非主体。
  41. - "perf":人物演唱镜头(正面/侧脸/半身,情绪表演)。
  42. 两类交替出现,情绪曲线从铺垫 → 蓄积 → 爆发 → 收束,与歌词段落对应。
  43. 3. 每个镜头的 imagePrompt 必须是「可独立投喂文生图模型」的完整英文提示词:
  44. 主体 + 动作/状态 + 环境 + 光线 + 镜头语言(焦段/景别/角度)+ 风格 + 画质词,
  45. 并在含人物时包含 appearanceAnchor 的英文版本。
  46. 4. 每个镜头的 videoPrompt 是「图生视频」的运镜描述(英文):
  47. 主体动作 + 镜头运动(push in / pull back / pan left / orbit / handheld)+ 节奏感,
  48. 不含画面内容描述(内容来自关键帧),6 秒内能完成。
  49. 5. 若某镜头需要唇形驱动(人物正脸演唱),把 "lipSync": true,其余为 false。
  50. 唇形驱动镜头总数建议 ≤ 4 个。
  51. 6. 所有镜头时长之和应接近歌曲总时长。`;
  52. }
  53. /** 口播稿的用户提示词(产线B) */
  54. export function buildScriptPrompt({ topic, durationSec = 90, audience = '', tone = '专业但口语化', points = [], extra = '' }) {
  55. const words = Math.round(durationSec * 3.6); // 中文口播约 3.5~4 字/秒
  56. return `写一篇可直接用于 AI 语音合成(TTS)的口播讲解稿。
  57. 【主题】${topic}
  58. 【目标时长】约 ${durationSec} 秒(约 ${words} 字,中文口播语速约 3.6 字/秒)
  59. ${audience ? `【受众】${audience}` : ''}
  60. 【语气】${tone}
  61. ${points.length ? `【必须覆盖的要点】\n- ${points.join('\n- ')}` : ''}
  62. ${extra ? `【其它】${extra}` : ''}
  63. 写作要求:
  64. 1. 口语化、短句为主,避免书面语与长难句(TTS 读长句会断气)。
  65. 2. 开头 3 秒必须有钩子(提问/反常识/利益点),结尾一句行动号召。
  66. 3. 数字与专有名词要写出来(会直接读出来),必要时用中文读法(如 "百分之三十")。`;
  67. }
  68. /** 产线B 讲解画面分镜的用户提示词 */
  69. export function buildTalkStoryboardPrompt({ script, durationSec, aspect = '16:9', shotCount = 8, project = '', hostAnchor = '' }) {
  70. return `为下面这篇口播稿设计 AI 生成的讲解视频分镜(纯 AI 生成画面,无实拍)。
  71. 【画面比例】${aspect}
  72. 【总时长】约 ${durationSec} 秒
  73. 【镜头数量】${shotCount} 个(必须控制在 ${shotCount}±2 个,这是硬性约束)
  74. 【单镜时长】每镜约 ${Math.round(durationSec / shotCount)} 秒,且**必须落在 4~10 秒之间**(AI 图生视频单段上限约 10 秒)
  75. ${project ? `【项目背景】${project}` : ''}
  76. ${hostAnchor ? `【讲者设定】${hostAnchor}(每个含讲者的镜头都必须原样复述该外观锚点)` : '【讲者设定】请先定义一位讲者的外观锚点 appearanceAnchor'}
  77. 【口播稿全文】
  78. ${script}
  79. 设计要求:
  80. 1. 每句/每小段口播都要有对应画面,画面与语义强相关(讲数据就给图表感画面,讲场景就给场景画面)。
  81. 2. 镜头两类交替:
  82. - "talking":讲者正面半身镜头(可选唇形驱动 lipSync:true,总数 ≤ 3 个)。
  83. - "broll":相关场景/物件/概念图(大量使用,占多数,降低口型不一致的观感风险)。
  84. 3. 每个镜头给出:start(秒)、duration(秒)、section(对应口播文本片段)、
  85. narration(该镜头对应的口播文字,用于生成字幕)、
  86. description(中文画面描述)、imagePrompt(英文,完整可投喂)、videoPrompt(英文运镜)。
  87. 4. 每个镜头的中文字幕文本放在 "subtitle" 字段(≤ 20 字,便于单行显示)。`;
  88. }
  89. /** 关键帧:把分镜镜头转成文生图 prompt(含人物一致性锚点) */
  90. export function buildKeyframePrompt(shot, { appearanceAnchor = '', style = '', aspect = '16:9' } = {}) {
  91. const parts = [];
  92. if (shot.imagePrompt) parts.push(shot.imagePrompt);
  93. else parts.push(shot.description || shot.prompt || '');
  94. if (shot.hasCharacter !== false && appearanceAnchor && !String(shot.imagePrompt || '').includes(appearanceAnchor.slice(0, 20))) {
  95. parts.push(`Character appearance (keep identical across shots): ${appearanceAnchor}`);
  96. }
  97. if (style) parts.push(style);
  98. parts.push('cinematic still frame, high detail, film grain, color graded, 4k');
  99. return parts.filter(Boolean).join(', ');
  100. }
  101. /** 视频生成:把分镜镜头转成图生视频 prompt */
  102. export function buildVideoPrompt(shot) {
  103. return shot.videoPrompt || 'subtle natural motion, slow push in, cinematic';
  104. }
  105. /** 画面比例 → 文生图尺寸 */
  106. export function aspectToSize(aspect = '16:9', tier = 'standard') {
  107. const map = {
  108. '16:9': { standard: '1536x864', large: '1792x1024' },
  109. '9:16': { standard: '864x1536', large: '1024x1792' },
  110. '1:1': { standard: '1024x1024', large: '1024x1024' },
  111. '4:3': { standard: '1152x864', large: '1365x1024' },
  112. };
  113. return (map[aspect] || map['16:9'])[tier] || '1024x1024';
  114. }
  115. /** 视频生成:比例 → 视频尺寸 */
  116. export function aspectToVideoSize(aspect = '16:9') {
  117. return aspect === '9:16' ? '720x1280' : aspect === '1:1' ? '960x960' : '1280x720';
  118. }
  119. /** 默认人物外观锚点模板(用户不给时使用) */
  120. export const DEFAULT_ANCHOR = {
  121. song: 'Asian woman in her late 20s, shoulder-length black hair with soft waves, natural makeup, wearing a deep navy oversized jacket over a white tee, calm melancholic gaze, cinematic presence',
  122. talk: 'Asian man in his 30s, short neat black hair, light stubble, wearing a charcoal knit sweater, warm approachable expression, professional presenter presence',
  123. };
  124. // ==================== 人物资产多视图(character-sheet)====================
  125. //
  126. // 出处:`_kb/07-即梦API工具箱/脚本/生成-人物资产v4流水线.js`(含 P5_PROMPT 原文)。
  127. // 这套四步流水线的价值:跨镜头不换脸。第④步多视图是后续所有含人物镜头的参考图。
  128. /** 把用户的中文人物设定拼成即梦够用的基础描述(保留用户原话,只补画质约束) */
  129. export function buildCharacterBase(description) {
  130. const d = String(description || '').trim().replace(/\s+/g, ' ');
  131. return (
  132. d +
  133. ' 写实摄影风格,非商业修图,保留轻微噪点,高光不过曝,阴影有细节;' +
  134. '表情自然松弛,眼神自然不失真;皮肤质感真实,脸部清晰、五官完整、无遮挡。'
  135. );
  136. }
  137. /**
  138. * 从人物设定里抽出「服装」短语,供 ②③④ 步复用。
  139. *
  140. * 为什么需要:KB 的 P2/P3/P5 prompt 把服装写死成「白色T恤 + 波点半身裙 + 草编包」,
  141. * 这会导致生成出来的人「脸是对的、衣服是错的」。服装必须由用户描述驱动。
  142. * 抽不出来就退回调用方给的兜底值,并让调用方显式知道用了兜底(不静默)。
  143. */
  144. export function extractOutfit(description, fallback = '白色T恤 + 波点半身裙') {
  145. const d = String(description || '');
  146. // 在服装提示词附近抓一个短语(中文服装词表)
  147. const m = d.match(
  148. /(?:身穿|穿着|穿|服装|搭配|着装|衣着)[::]?\s*([^,。;;,.\n]{2,40})/
  149. );
  150. if (m && m[1]) return m[1].trim();
  151. // 退一步:直接找带「衣/裙/裤/衫/外套/大衣/西装/卫衣/衬衫/背心」的短语
  152. const m2 = d.match(/([^,。;;,.\n]{0,16}(?:上衣|T恤|t恤|衬衫|卫衣|外套|大衣|西装|背心|裙|裤|袍)[^,。;;,.\n]{0,16})/);
  153. if (m2 && m2[1]) return m2[1].trim();
  154. return fallback;
  155. }
  156. /**
  157. * 四步 prompt 模板(②③④ 为图生图,参考上一步选定的成品)。
  158. * @param outfit 服装短语(来自用户设定,见 extractOutfit)
  159. */
  160. export const PORTRAIT_PROMPTS = {
  161. /** ① 定妆照(文生图,3:4,3 张候选,择优) */
  162. step1: (base) =>
  163. `${base} 画面聚焦面部,高曝低饱和,整体处于暗调静谧房间的慵懒氛围中,` +
  164. `呈现生活快照般的随手一拍抓拍风格。中景,光影斑驳,环境为大晴天,柔和光线笼罩,` +
  165. `阳光透过发丝照在脸上,突出冷白皮通透自然的肌肤状态,整体神态灵动又不失随性,唯美的氛围感。`,
  166. /** ② 上半身(图生图,参考①) */
  167. step2: (outfit = '白色T恤') =>
  168. '严格保留人物的五官结构、面部比例、发色发型和微表情,生成一张高清上半身正视图,' +
  169. '浅灰色影棚背景,高级商业摄影质感,真人写实风格。上半身完整入镜,正对镜头、头不歪,' +
  170. `五官全部露出,长发垂坠,瓷感白肌肤,佩戴简约饰品,服装为「${outfit}」的上身部分。`,
  171. /** ③ 全身(图生图,参考②) */
  172. step3: (outfit = '白色T恤 + 波点半身裙') =>
  173. '生成一张原创角色全身定妆照,浅灰色影棚背景,高级商业摄影质感,真人写实风格。' +
  174. '人物五官、妆容、发型、表情严格不变,微表情保留。画面构图与全身比例:全身定妆照,' +
  175. '完整展示头顶到脚底的全身比例,人物处于画面中央,A-pose,姿态放松松弛,背景干净无杂物。' +
  176. '光线与画质:柔和影棚主光,85mm/50mm 人像镜头,真实皮肤纹理,发丝清晰,' +
  177. `服装布料与纹理细腻自然。服装:${outfit}。`,
  178. /**
  179. * ④ 多视图(图生图,参考③,9:16)
  180. * ★ 原文照抄自 KB 的 P5_PROMPT:上 1/3 = 正/侧双头像特写,下 2/3 = 服装正面放大 + 背面全身。
  181. * 服装部分改为参数化(KB 原文写死为「白色T恤 + 波点半身裙 + 草编包」)。
  182. */
  183. step4: (outfit = '白色T恤 + 波点半身裙') => `生成图中人物的真人形象设定四视图,灰色摄影棚背景,整体为专业真人角色定妆参考板,规整排版,干净简洁,不是海报,不是杂志大片,不要剧情动作,不要复杂场景。
  184. 整体版式严格分为上下两部分:上半部分占画面高度三分之一,下半部分占画面高度三分之二。上方区域展示两张精细真人面部特写,左侧为正面面部特写,右侧为侧面面部特写。两张特写都以头部与肩颈为主,构图紧凑,清晰展示人物五官、骨骼结构、妆容、发型、头饰与神态。人物面部特征、发型、妆造、头部配饰必须与参考形象严格一致。皮肤质感真实自然,保留毛孔、细纹、绒毛、眉睫、碎发与皮肤受光后的细微起伏,不磨皮,不过度锐化,不做人像美容,呈现高解析真人摄影棚定妆照质感。
  185. 下方区域展示两张身体与服装参考图,左右排列。
  186. 左侧图片必须是放大后的正面服装展示图,人物采用标准 A-pose 站姿。这一张图的重点不是完整全身入镜,而是"人物尺度明显放大",让角色在画面中占据更大比例。左侧图片必须使用更近距离的正面取景,人物从肩颈下方开始入画,到鞋子结束,头部因画面放大而被完整裁切到画面之外。人物双肩、双臂、双手、躯干、腰部、腿部、鞋子必须完整保留,画面边缘只允许裁掉头部,不能裁掉手、脚或身体主体。
  187. 右侧图片为背面全身图,标准 A-pose,完整展示从头部到鞋子的背面全身。人物背对镜头,完整保留背面头部、发型背部、服装背面结构、鞋子与整体轮廓。
  188. 下方两张图必须为同一个人物、同一套服装、同一组配饰、同一身体比例。整体保持超模感修长比例,但不要夸张变形。服装设计、布料层次、鞋子、穿搭风格、服装结构与配饰样式必须严格遵循原图。服装:${outfit}。
  189. 光线只使用干净统一的摄影棚三点式打光:正左侧主光,正右侧辅助光,后右侧轮廓光。不要杂乱光效,不要彩色灯光,不要强氛围特效。背景为纯净灰色无缝影棚背景。
  190. 严格要求:上方两张脸部特写占画面上三分之一;下方两张身体图占画面下三分之二;左下图必须是"放大后的正面服装展示图",人物在画面中的占比明显更大,头部因近距离取景被完整裁切出画面;右下图必须是完整背面全身图;禁止左下图做成普通比例全身小图;禁止左下图出现任何头部信息;禁止左右服装不一致;禁止人物比例漂移;禁止 AI 油腻感、塑料皮肤感、过度美颜感、廉价 CG 感。`,
  191. };
  192. // ==================== 场景图(scene)====================
  193. /** 场景图(无人纯环境):为镜头铺环境,供关键帧做底 */
  194. export function buildScenePrompt(shot, { style = '', prevScene = '', aspect = '16:9' } = {}) {
  195. const parts = [];
  196. parts.push('Empty environment plate for a film shot, NO people, NO characters, pure scenery.');
  197. parts.push(shot.scenePrompt || shot.description || shot.prompt || '');
  198. if (shot.timeOfDay) parts.push(`Time of day: ${shot.timeOfDay}`);
  199. if (shot.location) parts.push(`Location: ${shot.location}`);
  200. if (prevScene) {
  201. parts.push(
  202. `Maintain visual continuity with the previous shot of the same sequence: ${prevScene}. ` +
  203. 'Keep the same color temperature, same lighting direction, same lens character so adjacent shots cut together seamlessly.'
  204. );
  205. }
  206. if (style) parts.push(style);
  207. parts.push(`Empty room, clean composition with clear space for a subject in the middle ground, ${aspect}`);
  208. parts.push('cinematic still frame, high detail, film grain, color graded, 4k');
  209. return parts.filter(Boolean).join(', ');
  210. }
  211. /** 基于场景图 + 人物资产图生成关键帧(有人物) */
  212. export function buildSceneKeyframePrompt(shot, { appearanceAnchor = '', style = '', aspect = '16:9' } = {}) {
  213. const parts = [];
  214. parts.push('Same location, same lighting, same lens as the reference environment image — only add the person into it.');
  215. parts.push(shot.imagePrompt || shot.description || '');
  216. if (appearanceAnchor && !String(shot.imagePrompt || '').includes(String(appearanceAnchor).slice(0, 20))) {
  217. parts.push(`Character appearance (keep identical across shots): ${appearanceAnchor}`);
  218. }
  219. if (style) parts.push(style);
  220. parts.push(`Shot on the same location as the reference background, ${aspect}`);
  221. parts.push('cinematic still frame, high detail, film grain, color graded, 4k');
  222. return parts.filter(Boolean).join(', ');
  223. }