import { createHash } from "node:crypto"; import { DEFAULT_PAGE_TEMPLATE_IDS, pageTemplate } from "./reference-first-page-template-registry.mjs"; import { resolvePlatformDesignProfile } from "./platform-design-profile-registry.mjs"; const text = (value, fallback = "") => typeof value === "string" && value.trim() ? value.trim() : fallback; const array = (value) => Array.isArray(value) ? value.map((item) => text(item)).filter(Boolean) : []; const digest = (value) => createHash("sha256").update(String(value)).digest("hex"); function normalizeCreativeBrief(value = {}) { const result = { category: text(value.category, "未明确类别商品"), form: text(value.form, "以三视图可见完整形态为准"), colors: array(value.colors), material_cues: array(value.material_cues), identity_cues: array(value.identity_cues), visible_selling_point_candidates: array(value.visible_selling_point_candidates), natural_support_and_use: array(value.natural_support_and_use), human_interactions: array(value.human_interactions), commercial_direction: text(value.commercial_direction, "真实商业产品摄影"), creative_audience_hypothesis: text(value.creative_audience_hypothesis), unsupported_copy_facts: array(value.unsupported_copy_facts), }; if (result.colors.length === 0) result.colors.push("颜色严格以三视图为准"); if (result.material_cues.length === 0) result.material_cues.push("材质和光泽严格以三视图为准"); if (result.identity_cues.length === 0) result.identity_cues.push("商品轮廓、比例、Logo和包装原有识别元素"); if (result.visible_selling_point_candidates.length < 2) throw new Error("CREATIVE_BRIEF_TWO_VISIBLE_SELLING_POINTS_REQUIRED"); if (result.natural_support_and_use.length === 0) throw new Error("CREATIVE_BRIEF_NATURAL_USE_REQUIRED"); return result; } function normalizePage(page = {}, index) { const id = String(index + 1).padStart(2, "0"); const templateId = text(page.template_id, DEFAULT_PAGE_TEMPLATE_IDS[index]); const template = pageTemplate(templateId); return { id, role: template.family, template_id: template.id, selection_reason: text(page.selection_reason, `该配方用于完成${template.page_goal}`), page_job: text(page.page_job, template.page_goal), scene: text(page.scene, index === 0 ? "纯白无缝背景" : "依据视觉设定板选择与商品自然使用关系一致的具体物理空间"), product_pose: text(page.product_pose, index === 0 ? "完整商品自然稳定居中,无遮挡" : template.relation), camera: text(page.camera, template.camera_language), human_relation: text(page.human_relation, template.family === "usage_scene" ? "根据商品自然使用方式建立明确人物关系" : "人物仅在有助于本页任务时出现"), props: array(page.props), composition: text(page.composition, template.composition), text_safe_zone: text(page.text_safe_zone, template.safe_zone), difference_from_peer: text(page.difference_from_peer, index === 0 ? "白底基准页无同定位页面" : "与同定位另一页采用不同任务、镜头、关系和构图"), }; } export function validateSetBlueprint(blueprint) { if (blueprint?.schema_version !== "2.0") throw new Error("SET_BLUEPRINT_VERSION_UNSUPPORTED"); if (!Array.isArray(blueprint.pages) || blueprint.pages.length !== 7) throw new Error("SET_BLUEPRINT_SEVEN_PAGES_REQUIRED"); const expected = ["white_background", "selling_point", "selling_point", "usage_scene", "usage_scene", "purchase_decision", "purchase_decision"]; if (blueprint.pages.some((page, index) => page.id !== String(index + 1).padStart(2, "0") || page.role !== expected[index])) { throw new Error("SET_BLUEPRINT_ROLE_ALLOCATION_INVALID"); } if (new Set(blueprint.pages.map((page) => page.template_id)).size !== 7) throw new Error("SET_BLUEPRINT_TEMPLATE_REPEATED"); for (const page of blueprint.pages) { for (const key of ["page_job", "scene", "product_pose", "camera", "composition", "text_safe_zone", "difference_from_peer"]) { if (!text(page[key])) throw new Error(`SET_BLUEPRINT_PAGE_FIELD_REQUIRED:${page.id}:${key}`); } } for (const pair of [[1, 2], [3, 4], [5, 6]]) { const left = blueprint.pages[pair[0]]; const right = blueprint.pages[pair[1]]; const differing = ["template_id", "page_job", "camera", "human_relation", "composition", "text_safe_zone"] .filter((key) => JSON.stringify(left[key]) !== JSON.stringify(right[key])).length; if (differing < 4) throw new Error(`SET_BLUEPRINT_PEER_DIFFERENCE_INSUFFICIENT:${left.role}`); } if (!blueprint.platform_strategy?.resolved_profile) throw new Error("SET_BLUEPRINT_PLATFORM_PROFILE_REQUIRED"); return blueprint; } export function compileSetBlueprint({ runId, generatedReferences, brief = {}, creativePlan = {} } = {}) { if (!text(runId)) throw new Error("SET_BLUEPRINT_RUN_ID_REQUIRED"); const references = array(generatedReferences); if (references.length !== 2) throw new Error("SET_BLUEPRINT_TWO_REFERENCES_REQUIRED"); const platform = resolvePlatformDesignProfile(brief.platform); const creativeBrief = normalizeCreativeBrief(creativePlan.product_creative_brief || creativePlan.product || {}); const rawPages = Array.isArray(creativePlan.pages) ? creativePlan.pages : []; if (rawPages.length !== 7) throw new Error("CREATIVE_PLAN_SEVEN_PAGES_REQUIRED"); const pages = rawPages.map(normalizePage); const profile = platform.profile; const blueprint = { schema_version: "2.0", run_id: runId, input_asset_sha256: references.map(digest), instruction: text(brief.instruction, "生成适合电商详情页的中文七图"), platform_strategy: { requested_platform: platform.requested_platform, resolved_profile: platform.resolved_profile, fallback_used: platform.fallback_used, profile_version: profile.version, profile_snapshot: profile, }, product_creative_brief: creativeBrief, visual_bible: { commercial_style: text(creativePlan.visual_bible?.commercial_style, creativeBrief.commercial_direction), palette: array(creativePlan.visual_bible?.palette).length ? array(creativePlan.visual_bible.palette) : creativeBrief.colors, lighting: text(creativePlan.visual_bible?.lighting, profile.color_lighting), prop_language: text(creativePlan.visual_bible?.prop_language, "道具克制且只解释商品关系"), background_materials: array(creativePlan.visual_bible?.background_materials), material_source: "商品形态、颜色、材质、纹理与光泽以产品三视图为唯一商品基准", setting_board_role: "产品视觉设定板只用于场景、氛围、构图与使用关系参考", }, pages, 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 })), copy_policy: { generation: "image_model_adds_no_new_text", verified_sources: ["current_user_prompt", "user_supplied_official_material"], inferred_product_facts_forbidden: true, }, }; return validateSetBlueprint(blueprint); } export function creativePlannerInstruction({ brief = {}, platformResolution = resolvePlatformDesignProfile(brief.platform) } = {}) { return `你是电商套图创意规划器,不是商品审核器。请根据输入的第一张产品三视图和第二张产品视觉设定板,为本次需求生成结构化设计方案。 用户需求:${text(brief.instruction, "生成适合电商详情页的中文七图")} 目标平台:${platformResolution.profile.label} 平台设计方向:${platformResolution.profile.rhythm};${platformResolution.profile.composition};${platformResolution.profile.scene_human_weight}。 只输出JSON对象,不要Markdown。结构必须为: { "product_creative_brief": { "category":"", "form":"", "colors":[""], "material_cues":[""], "identity_cues":[""], "visible_selling_point_candidates":["至少两个仅根据可见画面提出的卖点候选"], "natural_support_and_use":[""], "human_interactions":[""], "commercial_direction":"", "creative_audience_hypothesis":"", "unsupported_copy_facts":["不能从两张图证明的尺寸、性能、成分、功效等"] }, "visual_bible": {"commercial_style":"","palette":[""],"lighting":"","prop_language":"","background_materials":[""]}, "pages":[七个页面对象] } 七个页面按顺序必须使用以下定位与可选配方: 01 white_background:white_complete 02-03 selling_point:从 selling_form、selling_material、selling_operation、selling_brand 中选择两个不同配方 04-05 usage_scene:从 scene_core_environment、scene_human_interaction、scene_lifestyle、scene_mobile_outdoor、scene_installed 中选择两个不同配方 06-07 purchase_decision:从 decision_scale_fit、decision_usage_threshold、decision_context_match、decision_value_summary 中选择两个不同配方 每个页面对象必须包含:template_id、selection_reason、page_job、scene、product_pose、camera、human_relation、props数组、composition、text_safe_zone、difference_from_peer。 具体场景和动作必须适合当前商品;不要硬套不合理桌面、户外、穿戴或手持关系。六张非白底图不能只是同一模板换背景。同定位两页必须在任务、镜头、主体比例、人物/空间关系、视觉证据和文字区域中形成明确差异。图片后续独立生成,因此每页描述必须自足。不要把推断尺寸、材质、功效、成分、性能或包装清单写成商品事实。`; }