reference-first-set-blueprint.mjs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import { createHash } from "node:crypto";
  2. import { DEFAULT_PAGE_TEMPLATE_IDS, pageTemplate } from "./reference-first-page-template-registry.mjs";
  3. import { resolvePlatformDesignProfile } from "./platform-design-profile-registry.mjs";
  4. const text = (value, fallback = "") => typeof value === "string" && value.trim() ? value.trim() : fallback;
  5. const array = (value) => Array.isArray(value) ? value.map((item) => text(item)).filter(Boolean) : [];
  6. const digest = (value) => createHash("sha256").update(String(value)).digest("hex");
  7. function normalizeCreativeBrief(value = {}) {
  8. const result = {
  9. category: text(value.category, "未明确类别商品"),
  10. form: text(value.form, "以三视图可见完整形态为准"),
  11. colors: array(value.colors),
  12. material_cues: array(value.material_cues),
  13. identity_cues: array(value.identity_cues),
  14. visible_selling_point_candidates: array(value.visible_selling_point_candidates),
  15. natural_support_and_use: array(value.natural_support_and_use),
  16. human_interactions: array(value.human_interactions),
  17. commercial_direction: text(value.commercial_direction, "真实商业产品摄影"),
  18. creative_audience_hypothesis: text(value.creative_audience_hypothesis),
  19. unsupported_copy_facts: array(value.unsupported_copy_facts),
  20. };
  21. if (result.colors.length === 0) result.colors.push("颜色严格以三视图为准");
  22. if (result.material_cues.length === 0) result.material_cues.push("材质和光泽严格以三视图为准");
  23. if (result.identity_cues.length === 0) result.identity_cues.push("商品轮廓、比例、Logo和包装原有识别元素");
  24. if (result.visible_selling_point_candidates.length < 2) throw new Error("CREATIVE_BRIEF_TWO_VISIBLE_SELLING_POINTS_REQUIRED");
  25. if (result.natural_support_and_use.length === 0) throw new Error("CREATIVE_BRIEF_NATURAL_USE_REQUIRED");
  26. return result;
  27. }
  28. function normalizePage(page = {}, index) {
  29. const id = String(index + 1).padStart(2, "0");
  30. const templateId = text(page.template_id, DEFAULT_PAGE_TEMPLATE_IDS[index]);
  31. const template = pageTemplate(templateId);
  32. return {
  33. id,
  34. role: template.family,
  35. template_id: template.id,
  36. selection_reason: text(page.selection_reason, `该配方用于完成${template.page_goal}`),
  37. page_job: text(page.page_job, template.page_goal),
  38. scene: text(page.scene, index === 0 ? "纯白无缝背景" : "依据视觉设定板选择与商品自然使用关系一致的具体物理空间"),
  39. product_pose: text(page.product_pose, index === 0 ? "完整商品自然稳定居中,无遮挡" : template.relation),
  40. camera: text(page.camera, template.camera_language),
  41. human_relation: text(page.human_relation, template.family === "usage_scene" ? "根据商品自然使用方式建立明确人物关系" : "人物仅在有助于本页任务时出现"),
  42. props: array(page.props),
  43. composition: text(page.composition, template.composition),
  44. text_safe_zone: text(page.text_safe_zone, template.safe_zone),
  45. difference_from_peer: text(page.difference_from_peer, index === 0 ? "白底基准页无同定位页面" : "与同定位另一页采用不同任务、镜头、关系和构图"),
  46. };
  47. }
  48. export function validateSetBlueprint(blueprint) {
  49. if (blueprint?.schema_version !== "2.0") throw new Error("SET_BLUEPRINT_VERSION_UNSUPPORTED");
  50. if (!Array.isArray(blueprint.pages) || blueprint.pages.length !== 7) throw new Error("SET_BLUEPRINT_SEVEN_PAGES_REQUIRED");
  51. const expected = ["white_background", "selling_point", "selling_point", "usage_scene", "usage_scene", "purchase_decision", "purchase_decision"];
  52. if (blueprint.pages.some((page, index) => page.id !== String(index + 1).padStart(2, "0") || page.role !== expected[index])) {
  53. throw new Error("SET_BLUEPRINT_ROLE_ALLOCATION_INVALID");
  54. }
  55. if (new Set(blueprint.pages.map((page) => page.template_id)).size !== 7) throw new Error("SET_BLUEPRINT_TEMPLATE_REPEATED");
  56. for (const page of blueprint.pages) {
  57. for (const key of ["page_job", "scene", "product_pose", "camera", "composition", "text_safe_zone", "difference_from_peer"]) {
  58. if (!text(page[key])) throw new Error(`SET_BLUEPRINT_PAGE_FIELD_REQUIRED:${page.id}:${key}`);
  59. }
  60. }
  61. for (const pair of [[1, 2], [3, 4], [5, 6]]) {
  62. const left = blueprint.pages[pair[0]];
  63. const right = blueprint.pages[pair[1]];
  64. const differing = ["template_id", "page_job", "camera", "human_relation", "composition", "text_safe_zone"]
  65. .filter((key) => JSON.stringify(left[key]) !== JSON.stringify(right[key])).length;
  66. if (differing < 4) throw new Error(`SET_BLUEPRINT_PEER_DIFFERENCE_INSUFFICIENT:${left.role}`);
  67. }
  68. if (!blueprint.platform_strategy?.resolved_profile) throw new Error("SET_BLUEPRINT_PLATFORM_PROFILE_REQUIRED");
  69. return blueprint;
  70. }
  71. export function compileSetBlueprint({ runId, generatedReferences, brief = {}, creativePlan = {} } = {}) {
  72. if (!text(runId)) throw new Error("SET_BLUEPRINT_RUN_ID_REQUIRED");
  73. const references = array(generatedReferences);
  74. if (references.length !== 2) throw new Error("SET_BLUEPRINT_TWO_REFERENCES_REQUIRED");
  75. const platform = resolvePlatformDesignProfile(brief.platform);
  76. const creativeBrief = normalizeCreativeBrief(creativePlan.product_creative_brief || creativePlan.product || {});
  77. const rawPages = Array.isArray(creativePlan.pages) ? creativePlan.pages : [];
  78. if (rawPages.length !== 7) throw new Error("CREATIVE_PLAN_SEVEN_PAGES_REQUIRED");
  79. const pages = rawPages.map(normalizePage);
  80. const profile = platform.profile;
  81. const blueprint = {
  82. schema_version: "2.0",
  83. run_id: runId,
  84. input_asset_sha256: references.map(digest),
  85. instruction: text(brief.instruction, "生成适合电商详情页的中文七图"),
  86. platform_strategy: {
  87. requested_platform: platform.requested_platform,
  88. resolved_profile: platform.resolved_profile,
  89. fallback_used: platform.fallback_used,
  90. profile_version: profile.version,
  91. profile_snapshot: profile,
  92. },
  93. product_creative_brief: creativeBrief,
  94. visual_bible: {
  95. commercial_style: text(creativePlan.visual_bible?.commercial_style, creativeBrief.commercial_direction),
  96. palette: array(creativePlan.visual_bible?.palette).length ? array(creativePlan.visual_bible.palette) : creativeBrief.colors,
  97. lighting: text(creativePlan.visual_bible?.lighting, profile.color_lighting),
  98. prop_language: text(creativePlan.visual_bible?.prop_language, "道具克制且只解释商品关系"),
  99. background_materials: array(creativePlan.visual_bible?.background_materials),
  100. material_source: "商品形态、颜色、材质、纹理与光泽以产品三视图为唯一商品基准",
  101. setting_board_role: "产品视觉设定板只用于场景、氛围、构图与使用关系参考",
  102. },
  103. pages,
  104. difference_matrix: pages.slice(1).map((page) => ({ page_id: page.id, role: page.role, template_id: page.template_id, peer_difference: page.difference_from_peer })),
  105. copy_policy: {
  106. generation: "image_model_adds_no_new_text",
  107. verified_sources: ["current_user_prompt", "user_supplied_official_material"],
  108. inferred_product_facts_forbidden: true,
  109. },
  110. };
  111. return validateSetBlueprint(blueprint);
  112. }
  113. export function creativePlannerInstruction({ brief = {}, platformResolution = resolvePlatformDesignProfile(brief.platform) } = {}) {
  114. return `你是电商套图创意规划器,不是商品审核器。请根据输入的第一张产品三视图和第二张产品视觉设定板,为本次需求生成结构化设计方案。
  115. 用户需求:${text(brief.instruction, "生成适合电商详情页的中文七图")}
  116. 目标平台:${platformResolution.profile.label}
  117. 平台设计方向:${platformResolution.profile.rhythm};${platformResolution.profile.composition};${platformResolution.profile.scene_human_weight}。
  118. 只输出JSON对象,不要Markdown。结构必须为:
  119. {
  120. "product_creative_brief": {
  121. "category":"",
  122. "form":"",
  123. "colors":[""],
  124. "material_cues":[""],
  125. "identity_cues":[""],
  126. "visible_selling_point_candidates":["至少两个仅根据可见画面提出的卖点候选"],
  127. "natural_support_and_use":[""],
  128. "human_interactions":[""],
  129. "commercial_direction":"",
  130. "creative_audience_hypothesis":"",
  131. "unsupported_copy_facts":["不能从两张图证明的尺寸、性能、成分、功效等"]
  132. },
  133. "visual_bible": {"commercial_style":"","palette":[""],"lighting":"","prop_language":"","background_materials":[""]},
  134. "pages":[七个页面对象]
  135. }
  136. 七个页面按顺序必须使用以下定位与可选配方:
  137. 01 white_background:white_complete
  138. 02-03 selling_point:从 selling_form、selling_material、selling_operation、selling_brand 中选择两个不同配方
  139. 04-05 usage_scene:从 scene_core_environment、scene_human_interaction、scene_lifestyle、scene_mobile_outdoor、scene_installed 中选择两个不同配方
  140. 06-07 purchase_decision:从 decision_scale_fit、decision_usage_threshold、decision_context_match、decision_value_summary 中选择两个不同配方
  141. 每个页面对象必须包含:template_id、selection_reason、page_job、scene、product_pose、camera、human_relation、props数组、composition、text_safe_zone、difference_from_peer。
  142. 具体场景和动作必须适合当前商品;不要硬套不合理桌面、户外、穿戴或手持关系。六张非白底图不能只是同一模板换背景。同定位两页必须在任务、镜头、主体比例、人物/空间关系、视觉证据和文字区域中形成明确差异。图片后续独立生成,因此每页描述必须自足。不要把推断尺寸、材质、功效、成分、性能或包装清单写成商品事实。`;
  143. }