import { randomUUID } from 'node:crypto'; import { COST_UNIT, GEMINI_IMAGE_MODEL, generationReserveCny, SEEDREAM_IMAGE_MODEL } from '../../providers/new-api-image.mjs'; import { sha256, stableStringify } from '../../core/request-key.mjs'; import { loadPlatforms, loadTemplate } from './catalog.mjs'; import { domesticPromptConstraints, resolveDomesticAssetRule } from './domestic-platforms.mjs'; import { buildIdentityLock, buildStyleLock, NEGATIVE_CONSTRAINTS } from './locks.mjs'; const BANNED_PROMPT_TERMS = /\b(instagram|tiktok|facebook|rednote|xiaohongshu|pinterest|linkedin|twitter|google display|amazon|shopify|sponsored|like button|follow button|comment box|engagement metrics|timestamp badge|audio icon|app ui|platform ui)\b/gi; const FULL_SLOT_TEMPLATES = { H1: 'hero-image', H2: 'lifestyle-scene', H3: 'before-after', H4: 'ugc-style', H5: 'poster-banner', M1: 'lifestyle-scene', M2: 'detail-macro', M3: 'exploded-view', M4: 'ugc-style', M5: 'packaging', M6: 'size-spec', M7: 'infographic', M8: 'poster-banner', M9: 'social-media' }; const COMPACT = [ ['S1', 'hero-image', '电商主图'], ['S2', 'lifestyle-scene', '生活方式场景'], ['S3', 'detail-macro', '材质与细节'], ['S4', 'infographic', '核心卖点'], ['S5', 'size-spec', '尺寸与规格'], ['S6', 'packaging', '包装与清单'], ['S7', 'poster-banner', '营销收束图'] ]; const MAIN_IMAGE = [ ['A1', 'hero-image', '合规白底主图'], ['A2', 'infographic', '核心卖点副图'], ['A3', 'multi-angle-grid', '多角度副图'], ['A4', 'detail-macro', '细节副图'], ['A5', 'lifestyle-scene', '生活方式副图'], ['A6', 'multi-product', '款式/组合副图'], ['A7', 'packaging', '包装清单副图'], ['A8', 'size-spec', '尺寸参考副图'] ]; const DETAIL_PAGE = [ ['D1', 'poster-banner', '品牌 Hero Banner'], ['D2', 'before-after', '痛点与结果'], ['D3', 'infographic', '核心卖点'], ['D4', 'detail-macro', '材质工艺'], ['D5', 'exploded-view', '技术结构'], ['D6', 'size-spec', '规格/用法'], ['D7', 'packaging', '包装与系列'], ['D8', 'luxury-atmospherics', '品牌背书收束'] ]; const ANGLES = ['正面', '左前 45 度', '右前 45 度', '左侧', '右侧', '背面', '上视', '细节近景', '动态/穿着展示']; function detectPreset(request = '', explicit = '') { if (explicit) return explicit; const text = String(request).toLowerCase(); if (/14|十四|完整套图|h1|m9/.test(text)) return 'full'; if (/九角|9角|多角度|multi-angle|lookbook/.test(text)) return 'multi-angle'; if (/a\+|详情页|detail page|hero banner/.test(text)) return 'detail-page'; if (/广告|投放|ad creative|campaign|促销/.test(text)) return 'ad-creative'; if (/社媒|小红书|instagram|tiktok|feed|story|carousel|轮播/.test(text)) return 'social'; if (/亚马逊|amazon|主图|副图|listing image/.test(text)) return 'main-image'; return 'compact'; } function platformKey(value = '') { const text = String(value).toLowerCase(); if (/amazon|亚马逊/.test(text)) return 'amazon'; if (/shopify/.test(text)) return 'shopify'; if (/google/.test(text)) return 'google_display'; if (/facebook/.test(text)) return 'facebook_ad'; if (/linkedin/.test(text)) return 'linkedin_ad'; if (/story/.test(text)) return 'instagram_story'; if (/instagram/.test(text)) return 'instagram_feed'; if (/tiktok|抖音/.test(text)) return 'tiktok'; if (/pinterest/.test(text)) return 'pinterest'; if (/小红书|rednote|xiaohongshu/.test(text)) return 'rednote'; if (/\bx\b|twitter/.test(text)) return 'x'; return 'general_ecommerce'; } function interpolate(value, vars) { if (typeof value === 'string') return value.replace(/\{([A-Za-z0-9_]+)\}/g, (_, key) => vars[key] || ''); if (Array.isArray(value)) return value.map(item => interpolate(item, vars)); if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, interpolate(item, vars)])); return value; } function flattenPrompt(value) { return Object.entries(value || {}).map(([key, item]) => `${key}: ${Array.isArray(item) ? item.join(', ') : item}`).filter(part => !part.endsWith(': ')).join('; '); } export function sanitizeGeneratorPrompt(value) { return String(value || '').replace(BANNED_PROMPT_TERMS, '').replace(/\s{2,}/g, ' ').replace(/\s+([,;:.])/g, '$1').trim(); } export function deriveProductName(input = {}, analysis = {}) { const explicit = String(input.productName || '').trim(); if (explicit) return explicit; const identity = analysis.product_identity || {}; const identityName = String(identity.product_name || identity.productName || identity.name || '').trim(); if (identityName) return identityName; const namedFact = (analysis.visible_facts || []).find(fact => /^(商品名|产品名|品名|product[ _-]?name|name)$/i.test(String(fact?.field || '').trim())); if (String(namedFact?.value || '').trim()) return String(namedFact.value).trim(); const brand = String(identity.logo || '').trim(); const category = String(identity.category || '').trim(); if (brand && category && !category.toLowerCase().includes(brand.toLowerCase())) return `${brand}-${category}`; return brand || category || '未命名商品'; } function applyDimensions(item, width, height, ratio) { item.width = width; item.height = height; item.ratio = ratio; item.prompt = item.prompt.replace(/Canvas\s+\d+x\d+/i, `Canvas ${width}x${height}`); } function templatePrompt(template, input, title, analysis, platform) { const identity = analysis.product_identity || {}; const vars = { product_description: input.productDescription || [identity.category, identity.shape, identity.packaging].filter(Boolean).join(' ') || 'the exact referenced product', material_description: (identity.materials || []).join(', '), platform_style: platform.label, overlay_elements: input.requestedCopy || 'none', color: (input.brandColors || [])[0] || (identity.colors || [])[0] || '', product: input.productDescription || identity.category || 'product' }; const base = interpolate(template.prompt_template || {}, vars); const variant = input.style && template.variants?.[input.style]?.overrides ? interpolate(template.variants[input.style].overrides, vars) : {}; const categoryTip = template.category_tips?.[input.category || identity.category] || ''; const textPolicy = platform.text_policy; const requestedCopy = input.requestedCopy && !/不在生成图中渲染文字|无文字/.test(textPolicy) ? `Render only this exact short copy: “${input.requestedCopy}”.` : 'Do not render text unless explicitly supplied.'; return sanitizeGeneratorPrompt([ `OUTPUT PURPOSE — ${title}.`, buildIdentityLock(analysis, input), buildStyleLock(analysis, input), flattenPrompt({ ...base, ...variant }), categoryTip, `Canvas ${platform.width}x${platform.height}; safe zone: ${platform.safe_zone}; text policy: ${textPolicy}.`, requestedCopy, NEGATIVE_CONSTRAINTS ].filter(Boolean).join(' ')); } function itemFromTuple(tuple, index, input, analysis, locale, platformOverride) { const [outputId, templateId, title, note] = tuple; const template = loadTemplate(templateId, locale); const platforms = loadPlatforms(); const platform = platforms[platformOverride || platformKey(input.platform)] || platforms.general_ecommerce; const providerModel = input.modelPreference === 'seedream' || input.modelPreference === SEEDREAM_IMAGE_MODEL ? SEEDREAM_IMAGE_MODEL : GEMINI_IMAGE_MODEL; const domesticRule = resolveDomesticAssetRule({ platform: input.platform, preset: input.preset, outputId, templateId }); if (domesticRule) { const target = domesticRule.target || {}; const domesticPlatform = { ...platform, label: domesticRule.platformLabel, width: Number(target.width || platform.width), height: Number(target.height || platform.height), ratio: target.ratio || platform.ratio, safe_zone: target.safeZone || platform.safe_zone, text_policy: target.textPolicy || platform.text_policy }; const basePrompt = templatePrompt(template, input, note ? `${title};${note}` : title, analysis, domesticPlatform); return { outputId, outputKind: templateId, title, reason: note || `套图预设中的${title}`, templateId, templateName: template.name, width: domesticPlatform.width, height: domesticPlatform.height, ratio: domesticPlatform.ratio, safeZone: domesticPlatform.safe_zone, textPolicy: domesticPlatform.text_policy, providerModel, prompt: `${basePrompt} ${domesticPromptConstraints(domesticRule)}`, estimatedCostCny: generationReserveCny(providerModel, { qualityReview: input.qualityReview !== false }), costUnit: COST_UNIT, sortOrder: index, platformRule: domesticRule }; } return { outputId, outputKind: templateId, title, reason: note || `套图预设中的${title}`, templateId, templateName: template.name, width: platform.width, height: platform.height, ratio: platform.ratio, safeZone: platform.safe_zone, textPolicy: platform.text_policy, providerModel, prompt: templatePrompt(template, input, note ? `${title};${note}` : title, analysis, platform), estimatedCostCny: generationReserveCny(providerModel, { qualityReview: input.qualityReview !== false }), costUnit: COST_UNIT, sortOrder: index }; } function tuplesForPreset(preset, input) { if (preset === 'full') return Object.entries(FULL_SLOT_TEMPLATES).map(([id, template]) => [id, template, `${id} 套图模块`]); if (preset === 'main-image') return MAIN_IMAGE; if (preset === 'detail-page') return DETAIL_PAGE; if (preset === 'multi-angle') return ANGLES.map((angle, index) => [`V${index + 1}`, 'multi-angle-grid', `一致性多角度 ${index + 1}`, angle]); if (preset === 'ad-creative') { const platforms = Array.isArray(input.platforms) && input.platforms.length ? input.platforms : [input.platform || 'general_ecommerce']; return platforms.slice(0, 8).map((platform, index) => [`AD${index + 1}`, 'poster-banner', `${platform} 广告创意`, `为 ${platform} 独立设计,不是简单缩放`, platformKey(platform)]); } if (preset === 'social') { const count = /轮播|carousel/i.test(input.request || '') ? Math.min(Number(input.count || 7), 10) : Math.min(Number(input.count || 1), 10); return Array.from({ length: count }, (_, index) => [`C${index + 1}`, 'social-media', count > 1 ? `社媒轮播第 ${index + 1} 页` : '社媒视觉', index === 0 ? '三秒钩子封面' : index === count - 1 ? '行动引导收束' : '单页一个信息点']); } return COMPACT; } export function buildImageSetPlan(input = {}) { const request = String(input.request || input.prompt || '').trim(); if (!request) throw new Error('request 为必填项。'); const locale = input.locale === 'en' ? 'en' : 'zh'; const preset = detectPreset(request, input.preset); const analysis = input.analysis?.analysis || input.analysis || {}; const productName = deriveProductName(input, analysis); const tuples = tuplesForPreset(preset, { ...input, request }); if (!tuples.length || tuples.length > 14) throw new Error('输出计划必须包含 1–14 个项目。'); const items = tuples.map((tuple, index) => itemFromTuple(tuple, index, { ...input, request, preset }, analysis, locale, tuple[4])); if (preset === 'multi-angle') { for (const item of items) applyDimensions(item, 2048, 2560, '4:5'); } if (preset === 'detail-page') { applyDimensions(items[0], 3024, 1296, '21:9'); applyDimensions(items.at(-1), 3024, 1296, '21:9'); } const estimatedCostCny = Number(items.reduce((sum, item) => sum + (item.estimatedCostCny || 0), 0).toFixed(4)); const planCore = { schemaVersion: 3, locale, preset, request, platform: input.platform || 'general_ecommerce', productName, productDescription: input.productDescription || '', analysis, items, estimatedCostCny, costUnit: COST_UNIT, costBasis: 'trusted_usd_price_and_exchange_rate', qualityReview: input.qualityReview !== false, requiresConfirmation: true, qualityChecks: ['商品身份一致', '品牌色与商品色一致', 'Logo/文字不变形', '平台尺寸和安全区', '无虚构声明', '无平台 UI/账号/水印', '无明显生成缺陷'] }; const planHash = sha256(stableStringify(planCore)); return { ...planCore, planId: input.planId || `plan_${planHash.slice(0, 16)}`, planHash, createdAt: input.createdAt || new Date().toISOString(), confirmationMessage: `计划共 ${items.length} 张,按受信美元价格与汇率预计 ¥${estimatedCostCny.toFixed(2)}。确认张数、模型、提示词和人民币预算后才能生成。` }; } export function toPublicPlan(plan = {}) { const items = (plan.items || []).map(({ estimatedCostCny, costUnit, ...item }) => item); const { estimatedCostCny, costUnit, costBasis, confirmationMessage, ...publicPlan } = plan; return { ...publicPlan, items, confirmationMessage: `计划共 ${items.length} 张;确认张数、模型、尺寸、用途和提示词后即可生成。` }; } function restoreTrustedPlanCosts(plan = {}) { const qualityReview = plan.qualityReview !== false; const items = (plan.items || []).map(item => ({ ...item, estimatedCostCny: generationReserveCny(item.providerModel, { qualityReview }), costUnit: COST_UNIT })); return { ...plan, items, estimatedCostCny: Number(items.reduce((sum, item) => sum + item.estimatedCostCny, 0).toFixed(4)), costUnit: COST_UNIT, costBasis: 'trusted_usd_price_and_exchange_rate' }; } export function verifyPlan(plan) { const { planId, planHash, createdAt, confirmationMessage, ...planCore } = plan || {}; let verified = plan; let actual = sha256(stableStringify(planCore)); if (planHash && actual !== planHash) { const restored = restoreTrustedPlanCosts(plan); const { planId: ignoredPlanId, planHash: ignoredPlanHash, createdAt: ignoredCreatedAt, confirmationMessage: ignoredMessage, ...restoredCore } = restored; actual = sha256(stableStringify(restoredCore)); if (actual === planHash) verified = restored; } if (!planHash || actual !== planHash) throw new Error('计划完整性校验失败,请重新规划。'); if (!planId || !Array.isArray(verified.items) || !verified.items.length || verified.items.length > 14) throw new Error('计划结构无效。'); return verified; } export { detectPreset, platformKey };