Upload latest code and optimized prompts (v6)
This commit is contained in:
332
lib/batch-processor.js
Normal file
332
lib/batch-processor.js
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* 批量处理器
|
||||
* 支持5000+ SKU的批量图片生成
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 批量处理配置
|
||||
*/
|
||||
const DEFAULT_CONFIG = {
|
||||
concurrency: 1, // 并发数(建议保持1避免API限流)
|
||||
retryCount: 3, // 失败重试次数
|
||||
retryDelay: 5000, // 重试延迟(毫秒)
|
||||
imageDelay: 1500, // 图片间延迟(毫秒)
|
||||
skuDelay: 3000, // SKU间延迟(毫秒)
|
||||
saveProgress: true, // 是否保存进度
|
||||
progressFile: 'batch-progress.json'
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量处理器类
|
||||
*/
|
||||
class BatchProcessor {
|
||||
constructor(serverUrl, config = {}) {
|
||||
this.serverUrl = serverUrl || 'http://localhost:3000';
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
this.progress = {
|
||||
total: 0,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
results: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个SKU
|
||||
* @param {object} sku - SKU数据
|
||||
* @returns {Promise<object>} 处理结果
|
||||
*/
|
||||
async processSKU(sku) {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
console.log(`Processing SKU: ${sku.sku_id}`);
|
||||
|
||||
const response = await axios.post(`${this.serverUrl}/api/generate-sku`, {
|
||||
sku
|
||||
}, {
|
||||
timeout: 600000 // 10分钟超时(12张图)
|
||||
});
|
||||
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
console.log(`SKU ${sku.sku_id} completed in ${duration.toFixed(1)}s`);
|
||||
|
||||
return {
|
||||
sku_id: sku.sku_id,
|
||||
status: 'success',
|
||||
duration,
|
||||
...response.data
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
console.error(`SKU ${sku.sku_id} failed after ${duration.toFixed(1)}s:`, error.message);
|
||||
|
||||
return {
|
||||
sku_id: sku.sku_id,
|
||||
status: 'failed',
|
||||
duration,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 带重试的处理单个SKU
|
||||
* @param {object} sku - SKU数据
|
||||
* @returns {Promise<object>} 处理结果
|
||||
*/
|
||||
async processSKUWithRetry(sku) {
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 1; attempt <= this.config.retryCount; attempt++) {
|
||||
try {
|
||||
const result = await this.processSKU(sku);
|
||||
|
||||
if (result.status === 'success') {
|
||||
return result;
|
||||
}
|
||||
|
||||
lastError = result.error;
|
||||
|
||||
if (attempt < this.config.retryCount) {
|
||||
console.log(`Retrying SKU ${sku.sku_id} (attempt ${attempt + 1}/${this.config.retryCount})...`);
|
||||
await this.delay(this.config.retryDelay);
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error.message;
|
||||
|
||||
if (attempt < this.config.retryCount) {
|
||||
console.log(`Retrying SKU ${sku.sku_id} (attempt ${attempt + 1}/${this.config.retryCount})...`);
|
||||
await this.delay(this.config.retryDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sku_id: sku.sku_id,
|
||||
status: 'failed',
|
||||
error: lastError,
|
||||
attempts: this.config.retryCount
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量处理SKU列表
|
||||
* @param {array} skus - SKU数据数组
|
||||
* @param {function} onProgress - 进度回调
|
||||
* @returns {Promise<object>} 批量处理结果
|
||||
*/
|
||||
async processBatch(skus, onProgress = null) {
|
||||
this.progress = {
|
||||
total: skus.length,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
startTime: Date.now(),
|
||||
results: []
|
||||
};
|
||||
|
||||
console.log(`\n${'='.repeat(60)}`);
|
||||
console.log(`Starting batch processing: ${skus.length} SKUs`);
|
||||
console.log(`${'='.repeat(60)}\n`);
|
||||
|
||||
// 加载之前的进度(如果有)
|
||||
const existingProgress = this.loadProgress();
|
||||
const processedIds = new Set(existingProgress.map(r => r.sku_id));
|
||||
|
||||
// 过滤掉已处理的SKU
|
||||
const pendingSkus = skus.filter(sku => !processedIds.has(sku.sku_id));
|
||||
this.progress.results = existingProgress;
|
||||
this.progress.completed = existingProgress.filter(r => r.status === 'success').length;
|
||||
this.progress.failed = existingProgress.filter(r => r.status === 'failed').length;
|
||||
|
||||
if (existingProgress.length > 0) {
|
||||
console.log(`Resuming from previous progress: ${existingProgress.length} already processed`);
|
||||
}
|
||||
|
||||
// 串行处理(避免API限流)
|
||||
for (let i = 0; i < pendingSkus.length; i++) {
|
||||
const sku = pendingSkus[i];
|
||||
const overallIndex = existingProgress.length + i + 1;
|
||||
|
||||
console.log(`\n[${overallIndex}/${skus.length}] Processing ${sku.sku_id}...`);
|
||||
|
||||
const result = await this.processSKUWithRetry(sku);
|
||||
this.progress.results.push(result);
|
||||
|
||||
if (result.status === 'success') {
|
||||
this.progress.completed++;
|
||||
} else {
|
||||
this.progress.failed++;
|
||||
}
|
||||
|
||||
// 保存进度
|
||||
if (this.config.saveProgress) {
|
||||
this.saveProgress();
|
||||
}
|
||||
|
||||
// 进度回调
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
current: overallIndex,
|
||||
total: skus.length,
|
||||
completed: this.progress.completed,
|
||||
failed: this.progress.failed,
|
||||
lastResult: result
|
||||
});
|
||||
}
|
||||
|
||||
// SKU间延迟
|
||||
if (i < pendingSkus.length - 1) {
|
||||
await this.delay(this.config.skuDelay);
|
||||
}
|
||||
}
|
||||
|
||||
this.progress.endTime = Date.now();
|
||||
this.progress.totalDuration = (this.progress.endTime - this.progress.startTime) / 1000;
|
||||
|
||||
console.log(`\n${'='.repeat(60)}`);
|
||||
console.log(`Batch processing complete!`);
|
||||
console.log(`Total: ${skus.length}, Success: ${this.progress.completed}, Failed: ${this.progress.failed}`);
|
||||
console.log(`Duration: ${this.progress.totalDuration.toFixed(1)}s`);
|
||||
console.log(`${'='.repeat(60)}\n`);
|
||||
|
||||
return {
|
||||
total: skus.length,
|
||||
completed: this.progress.completed,
|
||||
failed: this.progress.failed,
|
||||
duration: this.progress.totalDuration,
|
||||
results: this.progress.results
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟函数
|
||||
* @param {number} ms - 毫秒数
|
||||
*/
|
||||
delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存进度到文件
|
||||
*/
|
||||
saveProgress() {
|
||||
try {
|
||||
const progressPath = path.join(process.cwd(), this.config.progressFile);
|
||||
fs.writeFileSync(progressPath, JSON.stringify(this.progress.results, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Failed to save progress:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载之前的进度
|
||||
* @returns {array} 之前的处理结果
|
||||
*/
|
||||
loadProgress() {
|
||||
try {
|
||||
const progressPath = path.join(process.cwd(), this.config.progressFile);
|
||||
if (fs.existsSync(progressPath)) {
|
||||
const content = fs.readFileSync(progressPath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load progress:', error.message);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除进度文件
|
||||
*/
|
||||
clearProgress() {
|
||||
try {
|
||||
const progressPath = path.join(process.cwd(), this.config.progressFile);
|
||||
if (fs.existsSync(progressPath)) {
|
||||
fs.unlinkSync(progressPath);
|
||||
console.log('Progress cleared');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to clear progress:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成批处理报告
|
||||
* @returns {object} 报告数据
|
||||
*/
|
||||
generateReport() {
|
||||
const successResults = this.progress.results.filter(r => r.status === 'success');
|
||||
const failedResults = this.progress.results.filter(r => r.status === 'failed');
|
||||
|
||||
const totalImages = successResults.reduce((sum, r) => {
|
||||
return sum + (r.summary?.success || 0);
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
summary: {
|
||||
total_skus: this.progress.total,
|
||||
successful_skus: this.progress.completed,
|
||||
failed_skus: this.progress.failed,
|
||||
success_rate: ((this.progress.completed / this.progress.total) * 100).toFixed(1) + '%',
|
||||
total_images_generated: totalImages,
|
||||
total_duration: this.progress.totalDuration?.toFixed(1) + 's',
|
||||
average_time_per_sku: (this.progress.totalDuration / this.progress.total).toFixed(1) + 's'
|
||||
},
|
||||
failed_skus: failedResults.map(r => ({
|
||||
sku_id: r.sku_id,
|
||||
error: r.error
|
||||
})),
|
||||
successful_skus: successResults.map(r => ({
|
||||
sku_id: r.sku_id,
|
||||
images: r.summary
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件加载SKU列表
|
||||
* @param {string} filePath - 文件路径(JSON或CSV)
|
||||
* @returns {array} SKU数组
|
||||
*/
|
||||
function loadSKUsFromFile(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
|
||||
if (ext === '.json') {
|
||||
const data = JSON.parse(content);
|
||||
return Array.isArray(data) ? data : [data];
|
||||
}
|
||||
|
||||
// 简单CSV解析(假设第一行是表头)
|
||||
if (ext === '.csv') {
|
||||
const lines = content.split('\n').filter(line => line.trim());
|
||||
const headers = lines[0].split(',').map(h => h.trim());
|
||||
|
||||
return lines.slice(1).map(line => {
|
||||
const values = line.split(',').map(v => v.trim());
|
||||
const obj = {};
|
||||
headers.forEach((h, i) => {
|
||||
obj[h] = values[i];
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error('Unsupported file format. Use .json or .csv');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BatchProcessor,
|
||||
loadSKUsFromFile,
|
||||
DEFAULT_CONFIG
|
||||
};
|
||||
|
||||
|
||||
|
||||
332
lib/brain.js
Normal file
332
lib/brain.js
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* Brain决策逻辑
|
||||
* 调用LLM分析产品信息并生成12张图的规划
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
|
||||
// 读取Brain System Prompt
|
||||
const brainPromptPath = path.join(__dirname, '../prompts/brain-system.md');
|
||||
let brainSystemPrompt = '';
|
||||
|
||||
try {
|
||||
brainSystemPrompt = fs.readFileSync(brainPromptPath, 'utf-8');
|
||||
} catch (err) {
|
||||
console.error('Failed to load brain system prompt:', err.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用LLM生成图片规划
|
||||
* @param {object} sku - SKU数据
|
||||
* @param {object} options - 选项
|
||||
* @param {string} options.apiKey - API密钥
|
||||
* @param {string} options.apiUrl - API地址
|
||||
* @param {string} options.model - 模型名称
|
||||
* @returns {Promise<object>} Brain输出的图片规划
|
||||
*/
|
||||
async function generateImagePlan(sku, options = {}) {
|
||||
const {
|
||||
apiKey = process.env.API_KEY,
|
||||
apiUrl = 'https://api2img.shubiaobiao.com/v1/chat/completions',
|
||||
model = 'gemini-3-pro-preview'
|
||||
} = options;
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('API_KEY is required');
|
||||
}
|
||||
|
||||
// 构建用户消息
|
||||
const userMessage = `请根据以下产品信息,规划12张电商图片(6张主图 + 6张A+图)的内容策略。
|
||||
|
||||
## 产品信息
|
||||
|
||||
\`\`\`json
|
||||
${JSON.stringify(sku, null, 2)}
|
||||
\`\`\`
|
||||
|
||||
请严格按照System Prompt中的输出格式返回JSON。`;
|
||||
|
||||
try {
|
||||
const response = await axios.post(apiUrl, {
|
||||
model: model,
|
||||
messages: [
|
||||
{ role: 'system', content: brainSystemPrompt },
|
||||
{ role: 'user', content: userMessage }
|
||||
],
|
||||
temperature: 0.7,
|
||||
max_tokens: 8000
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
},
|
||||
timeout: 120000 // 2分钟超时
|
||||
});
|
||||
|
||||
// 解析响应
|
||||
let content = response.data.choices?.[0]?.message?.content;
|
||||
|
||||
if (!content) {
|
||||
throw new Error('Empty response from LLM');
|
||||
}
|
||||
|
||||
// 清理响应内容
|
||||
content = cleanLLMResponse(content);
|
||||
|
||||
// 解析JSON
|
||||
const plan = parseJSONFromResponse(content);
|
||||
|
||||
// 验证输出格式
|
||||
validatePlan(plan);
|
||||
|
||||
return plan;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Brain generation error:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理LLM响应
|
||||
* @param {string} content - 原始响应内容
|
||||
* @returns {string} 清理后的内容
|
||||
*/
|
||||
function cleanLLMResponse(content) {
|
||||
// 移除 <think> 块
|
||||
content = content.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
|
||||
|
||||
// 移除可能的markdown标记
|
||||
content = content.replace(/^```json\s*/i, '');
|
||||
content = content.replace(/\s*```$/i, '');
|
||||
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中解析JSON
|
||||
* @param {string} content - 响应内容
|
||||
* @returns {object} 解析后的JSON对象
|
||||
*/
|
||||
function parseJSONFromResponse(content) {
|
||||
// 尝试直接解析
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch (e) {
|
||||
// 尝试提取JSON块
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
return JSON.parse(jsonMatch[0]);
|
||||
} catch (e2) {
|
||||
// 尝试修复常见问题
|
||||
let fixed = jsonMatch[0];
|
||||
|
||||
// 修复尾部逗号
|
||||
fixed = fixed.replace(/,\s*([}\]])/g, '$1');
|
||||
|
||||
// 修复未闭合的字符串
|
||||
fixed = fixed.replace(/:\s*"([^"]*?)(?=\s*[,}\]])/g, ': "$1"');
|
||||
|
||||
try {
|
||||
return JSON.parse(fixed);
|
||||
} catch (e3) {
|
||||
throw new Error('Failed to parse JSON from LLM response: ' + e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error('No valid JSON found in LLM response');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Brain输出的计划
|
||||
* @param {object} plan - 图片规划
|
||||
*/
|
||||
function validatePlan(plan) {
|
||||
if (!plan.analysis) {
|
||||
throw new Error('Missing analysis in plan');
|
||||
}
|
||||
|
||||
if (!plan.images || !Array.isArray(plan.images)) {
|
||||
throw new Error('Missing or invalid images array in plan');
|
||||
}
|
||||
|
||||
if (plan.images.length !== 12) {
|
||||
console.warn(`Expected 12 images, got ${plan.images.length}`);
|
||||
}
|
||||
|
||||
// 检查必要字段
|
||||
const requiredFields = ['id', 'type', 'ai_prompt'];
|
||||
for (const image of plan.images) {
|
||||
for (const field of requiredFields) {
|
||||
if (!image[field]) {
|
||||
throw new Error(`Missing ${field} in image ${image.id || 'unknown'}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查图片ID是否正确
|
||||
const expectedIds = [
|
||||
'Main_01', 'Main_02', 'Main_03', 'Main_04', 'Main_05', 'Main_06',
|
||||
'APlus_01', 'APlus_02', 'APlus_03', 'APlus_04', 'APlus_05', 'APlus_06'
|
||||
];
|
||||
|
||||
const actualIds = plan.images.map(img => img.id);
|
||||
const missingIds = expectedIds.filter(id => !actualIds.includes(id));
|
||||
|
||||
if (missingIds.length > 0) {
|
||||
console.warn(`Missing image IDs: ${missingIds.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成默认的图片规划(当Brain调用失败时使用)
|
||||
* @param {object} sku - SKU数据
|
||||
* @returns {object} 默认的图片规划
|
||||
*/
|
||||
function generateDefaultPlan(sku) {
|
||||
const topSellingPoints = sku.selling_points.slice(0, 3).map(sp => sp.key);
|
||||
const productColor = sku.color.name;
|
||||
const productName = sku.product_name;
|
||||
|
||||
return {
|
||||
analysis: {
|
||||
product_category: 'Pet Recovery Cone',
|
||||
core_selling_points: topSellingPoints,
|
||||
background_strategy: 'light',
|
||||
logo_color: 'red',
|
||||
visual_style: 'Warm, caring home environment'
|
||||
},
|
||||
images: [
|
||||
{
|
||||
id: 'Main_01',
|
||||
type: 'Hero Scene + Key Benefits',
|
||||
purpose: '首图决定点击率,展示产品使用状态+核心卖点',
|
||||
layout_description: `上60%:猫咪佩戴${productColor}${productName}的居家场景。下40%:浅色Banner展示3个核心卖点。`,
|
||||
ai_prompt: `Professional Amazon main image, 1:1 square format. A beautiful cat wearing a ${productColor} soft cone collar in a cozy modern home interior. Natural soft lighting, warm atmosphere. Bottom section shows a light blue rounded banner with "DESIGNED FOR COMFORTABLE RECOVERY" text and three small product detail images highlighting key features. Clean, professional pet product photography style. --ar 1:1`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'Main_02',
|
||||
type: 'Product Detail + Craftsmanship',
|
||||
purpose: '展示产品结构和工艺细节',
|
||||
layout_description: `产品平铺俯视图,配合2-3个细节放大框展示材质和工艺`,
|
||||
ai_prompt: `Professional product flat lay image, 1:1 square format. ${productColor} soft cone collar displayed from above on white background. Two circular detail callouts showing: 1) waterproof PU outer layer texture, 2) soft cotton inner lining. Clean product photography, even lighting, high detail. Brand logo "TOUCHDOG®" visible on right petal. --ar 1:1`,
|
||||
logo_placement: { position: 'none', type: 'none', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'Main_03',
|
||||
type: 'Function Demonstration',
|
||||
purpose: '展示可调节绑带等功能特性',
|
||||
layout_description: `主场景展示佩戴状态,配合细节图展示调节功能`,
|
||||
ai_prompt: `Professional Amazon feature demonstration image, 1:1 square format. Main image shows cat wearing ${productColor} cone, looking comfortable. Detail callouts show: adjustable velcro strap being secured, snug fit around neck. Text overlay "ADJUSTABLE STRAP FOR A SECURE FIT". Light blue background with paw print decorations. --ar 1:1`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'Main_04',
|
||||
type: 'Use Cases Grid',
|
||||
purpose: '展示4种适用场景',
|
||||
layout_description: `四宫格布局,展示术后护理、滴药、剪指甲、梳毛四个场景`,
|
||||
ai_prompt: `Professional Amazon use case image, 1:1 square format. Four-panel grid showing: 1) cat resting after surgery wearing cone, 2) owner applying eye drops to cat, 3) owner trimming cat's nails, 4) cat being groomed. Each panel labeled: "POSTOPERATIVE CARE", "EYE MEDICATION", "NAIL TRIMMING", "GROOMING". Light blue header banner "APPLICABLE SCENARIOS". --ar 1:1`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'Main_05',
|
||||
type: 'Size & Specifications',
|
||||
purpose: '展示尺寸规格信息',
|
||||
layout_description: `产品尺寸标注图配合尺码对照表`,
|
||||
ai_prompt: `Professional Amazon size guide image, 1:1 square format. ${productColor} cone collar with dimension annotations showing depth measurement (${sku.specs.depth_cm}cm). Clean size chart showing XS to XL with neck circumference ranges. Professional infographic style, clear readable text. Light background. --ar 1:1`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'Main_06',
|
||||
type: 'Brand & Benefits Summary',
|
||||
purpose: '强化品牌认知和卖点汇总',
|
||||
layout_description: `品牌Logo突出展示,配合核心卖点图标`,
|
||||
ai_prompt: `Professional Amazon brand summary image, 1:1 square format. Touchdog brand logo prominently displayed. Key benefit icons with text: "65g Ultra Light", "Waterproof PU", "Breathable Cotton", "Adjustable Fit", "Foldable Design". Cat wearing ${productColor} cone as background element. Premium brand aesthetic. --ar 1:1`,
|
||||
logo_placement: { position: 'center', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'APlus_01',
|
||||
type: 'Brand Banner',
|
||||
purpose: '品牌形象首图',
|
||||
layout_description: `品牌名+产品名+生活场景大图`,
|
||||
ai_prompt: `Professional Amazon A+ banner image, 970x600px landscape. Beautiful cat wearing ${productColor} Touchdog soft cone collar in warm modern home interior. Large "TOUCHDOG" brand text and "CAT SOFT CONE COLLAR" product name overlaid. Lifestyle photography, natural lighting, cozy atmosphere. --ar 3:2`,
|
||||
logo_placement: { position: 'top-left', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'APlus_02',
|
||||
type: 'Competitor Comparison',
|
||||
purpose: '竞品对比展示优势',
|
||||
layout_description: `左侧我们的产品(彩色+优点),右侧传统产品(灰色+缺点)`,
|
||||
ai_prompt: `Professional Amazon A+ comparison image, 970x600px landscape. Left side (colored): Touchdog ${productColor} soft cone with green checkmark, labeled "OUR" with benefits "CLOUD-LIGHT COMFORT", "WIDER & CLEARER", "FOLDABLE & PORTABLE". Right side (grayscale): Generic plastic cone with red X, labeled "OTHER" with drawbacks "HEAVY & BULKY", "BLOCKS VISION & MOVEMENT", "HARD TO STORE". Center shows cat wearing our product. Warm beige background with paw prints. --ar 3:2`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' },
|
||||
is_comparison: true
|
||||
},
|
||||
{
|
||||
id: 'APlus_03',
|
||||
type: 'Benefits Grid',
|
||||
purpose: '核心卖点展示',
|
||||
layout_description: `三宫格展示3个核心卖点`,
|
||||
ai_prompt: `Professional Amazon A+ benefits image, 970x600px landscape. Three-panel layout showing key features: 1) "STURDY AND BREATHABLE" with fabric texture close-up, 2) "EASY TO CLEAN" with dimension annotation ${sku.specs.depth_cm}cm, 3) "REINFORCED STITCHING" with stitching detail. Header "ENGINEERED FOR UNCOMPROMISED COMFORT". Warm beige background. --ar 3:2`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'APlus_04',
|
||||
type: 'Features Grid',
|
||||
purpose: '功能场景展示',
|
||||
layout_description: `四宫格展示功能:易清洁、不影响进食、可翻折、360°舒适`,
|
||||
ai_prompt: `Professional Amazon A+ features image, 970x600px landscape. Four vertical panels: 1) "HYGIENIC & EASY TO CLEAN" showing water-resistant surface, 2) "UNRESTRICTED EATING/DRINKING" showing cat eating while wearing cone, 3) "REVERSIBLE WEAR" showing flip-over design, 4) "360° COMFORT" showing cat sleeping peacefully. Warm beige background, consistent styling. --ar 3:2`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'APlus_05',
|
||||
type: 'Material & Craftsmanship',
|
||||
purpose: '材质工艺展示',
|
||||
layout_description: `材质剖面和工艺细节展示`,
|
||||
ai_prompt: `Professional Amazon A+ material image, 970x600px landscape. Close-up details of ${productColor} cone showing: waterproof PU outer layer with water droplets, soft cotton inner lining texture, reinforced stitching seams, embroidered TOUCHDOG logo. Technical infographic style with material callouts. Light background. --ar 3:2`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'APlus_06',
|
||||
type: 'Size Guide',
|
||||
purpose: '尺寸选择指南',
|
||||
layout_description: `完整尺码表+测量指南`,
|
||||
ai_prompt: `Professional Amazon A+ size guide image, 970x600px landscape. Comprehensive size chart showing all sizes (XS-XL) with neck circumference and depth measurements in both cm and inches. Illustration showing how to measure pet's neck. Clear, readable table format. Helpful sizing tips. Professional infographic design. --ar 3:2`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 主入口:生成图片规划
|
||||
* @param {object} sku - SKU数据
|
||||
* @param {object} options - 选项
|
||||
* @returns {Promise<object>} 图片规划
|
||||
*/
|
||||
async function planImages(sku, options = {}) {
|
||||
try {
|
||||
// 尝试调用LLM生成计划
|
||||
const plan = await generateImagePlan(sku, options);
|
||||
console.log('Brain generated plan successfully');
|
||||
return plan;
|
||||
} catch (error) {
|
||||
console.error('Brain failed, using default plan:', error.message);
|
||||
// 失败时使用默认计划
|
||||
return generateDefaultPlan(sku);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
planImages,
|
||||
generateImagePlan,
|
||||
generateDefaultPlan,
|
||||
validatePlan
|
||||
};
|
||||
|
||||
|
||||
|
||||
369
lib/constraint-injector.js
Normal file
369
lib/constraint-injector.js
Normal file
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* 约束注入器
|
||||
* 自动将各类约束追加到AI生图Prompt中
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 读取约束模板
|
||||
const constraintsDir = path.join(__dirname, '../prompts/constraints');
|
||||
|
||||
/**
|
||||
* 简单的模板引擎,替换 {{variable}} 占位符
|
||||
* @param {string} template - 模板字符串
|
||||
* @param {object} data - 数据对象
|
||||
* @returns {string} 替换后的字符串
|
||||
*/
|
||||
function renderTemplate(template, data) {
|
||||
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
||||
const keys = key.trim().split('.');
|
||||
let value = data;
|
||||
for (const k of keys) {
|
||||
if (value && typeof value === 'object') {
|
||||
// 处理数组索引 selling_points[0]
|
||||
const arrayMatch = k.match(/^(\w+)\[(\d+)\]$/);
|
||||
if (arrayMatch) {
|
||||
value = value[arrayMatch[1]]?.[parseInt(arrayMatch[2])];
|
||||
} else {
|
||||
value = value[k];
|
||||
}
|
||||
} else {
|
||||
return match; // 保留原占位符
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(', ');
|
||||
}
|
||||
|
||||
return value !== undefined ? String(value) : match;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Handlebars风格的循环 {{#each array}}...{{/each}}
|
||||
* @param {string} template - 模板字符串
|
||||
* @param {object} data - 数据对象
|
||||
* @returns {string} 处理后的字符串
|
||||
*/
|
||||
function processEachBlocks(template, data) {
|
||||
const eachRegex = /\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g;
|
||||
|
||||
return template.replace(eachRegex, (match, arrayName, content) => {
|
||||
const array = data[arrayName];
|
||||
if (!Array.isArray(array)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return array.map(item => {
|
||||
// 处理 {{#if field}} 条件
|
||||
let processed = content.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (m, field, ifContent) => {
|
||||
return item[field] ? ifContent : '';
|
||||
});
|
||||
|
||||
// 替换当前项的字段
|
||||
processed = processed.replace(/\{\{(\w+)\}\}/g, (m, field) => {
|
||||
return item[field] !== undefined ? String(item[field]) : m;
|
||||
});
|
||||
|
||||
return processed;
|
||||
}).join('\n');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成产品一致性约束
|
||||
* @param {object} sku - SKU数据
|
||||
* @returns {string} 约束文本
|
||||
*/
|
||||
function generateProductConsistencyConstraint(sku) {
|
||||
const template = `
|
||||
=== PRODUCT CONSISTENCY REQUIREMENTS (CRITICAL - DO NOT IGNORE) ===
|
||||
|
||||
The product shown in this image must EXACTLY match the reference images provided.
|
||||
|
||||
PRODUCT SPECIFICATIONS:
|
||||
- Product Type: ${sku.product_name}
|
||||
- Color: ${sku.color.hex} (${sku.color.name})
|
||||
- Color Series: ${sku.color.series || 'Standard'}
|
||||
- Edge/Binding Color: ${sku.color.edge_color || sku.color.hex}
|
||||
|
||||
STRUCTURAL REQUIREMENTS:
|
||||
- Shape: Soft cone collar with petal-like segments (NOT rigid plastic cone)
|
||||
- Segments: Multiple soft fabric petals radiating from center opening
|
||||
- Opening: Circular neck opening with ribbed cotton binding
|
||||
- Closure: Velcro strap closure system
|
||||
|
||||
BRAND LOGO ON PRODUCT:
|
||||
- Text: "TOUCHDOG®" (UPPERCASE with ® symbol)
|
||||
- Position: On one petal segment, typically right side (2-3 o'clock position when viewed from front)
|
||||
- Style: Embroidered/stitched into fabric, slightly recessed
|
||||
- Color: Darker or contrasting shade matching the product color
|
||||
|
||||
TEXTURE & MATERIAL APPEARANCE:
|
||||
- Outer Layer: Smooth, slightly glossy waterproof PU fabric
|
||||
- Inner Layer: Visible soft cotton lining at edges
|
||||
- Stitching: Reinforced seams, visible quilted pattern on petals
|
||||
- Edge Binding: Ribbed cotton binding in ${sku.color.edge_color || 'matching color'}
|
||||
|
||||
CRITICAL PROHIBITIONS:
|
||||
- DO NOT alter the product shape or structure
|
||||
- DO NOT change the color from specified hex value
|
||||
- DO NOT move or resize the embroidered logo
|
||||
- DO NOT add any elements not present in reference images
|
||||
- DO NOT stylize, reimagine, or "improve" the product design
|
||||
- DO NOT generate rigid plastic cone - must be SOFT fabric cone
|
||||
|
||||
=== END PRODUCT CONSISTENCY REQUIREMENTS ===
|
||||
`;
|
||||
return template.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成卖点合规约束
|
||||
* @param {object} sku - SKU数据
|
||||
* @returns {string} 约束文本
|
||||
*/
|
||||
function generateSellingPointConstraint(sku) {
|
||||
const sellingPointsList = sku.selling_points.map(sp => {
|
||||
let line = `- ${sp.title_en}`;
|
||||
if (sp.value) line += ` (Value: ${sp.value})`;
|
||||
if (sp.description_en) line += `: ${sp.description_en}`;
|
||||
return line;
|
||||
}).join('\n');
|
||||
|
||||
const visualPromptsList = sku.selling_points
|
||||
.filter(sp => sp.visual_prompt)
|
||||
.map(sp => `- ${sp.key}: ${sp.visual_prompt}`)
|
||||
.join('\n');
|
||||
|
||||
const useCasesList = sku.use_cases
|
||||
.map(uc => `- ${uc.name_en}`)
|
||||
.join('\n');
|
||||
|
||||
const template = `
|
||||
=== SELLING POINTS COMPLIANCE (LEGAL REQUIREMENT) ===
|
||||
|
||||
ALL product claims, features, and specifications shown in this image must come EXACTLY from the provided data.
|
||||
|
||||
APPROVED SELLING POINTS (use ONLY these):
|
||||
${sellingPointsList}
|
||||
|
||||
APPROVED SPECIFICATIONS (use EXACT values):
|
||||
- Weight: ${sku.specs.weight}
|
||||
- Depth: ${sku.specs.depth_cm}cm
|
||||
- Available Sizes: ${sku.specs.sizes.join(', ')}
|
||||
- Outer Material: ${sku.specs.materials?.outer || 'Waterproof PU'}
|
||||
- Inner Material: ${sku.specs.materials?.inner || 'Cotton blend'}
|
||||
|
||||
APPROVED USE CASES:
|
||||
${useCasesList}
|
||||
|
||||
PROHIBITED ACTIONS:
|
||||
- DO NOT invent new features or benefits not listed above
|
||||
- DO NOT modify numerical values (no rounding, no approximation)
|
||||
- DO NOT use superlatives: "best", "first", "only", "most", "#1", "leading"
|
||||
- DO NOT make comparative claims without data: "better than", "superior to"
|
||||
- DO NOT add health claims not approved by regulations
|
||||
- DO NOT promise specific outcomes: "guaranteed", "100% effective"
|
||||
|
||||
VISUAL REPRESENTATION OF SELLING POINTS:
|
||||
${visualPromptsList}
|
||||
|
||||
=== END SELLING POINTS COMPLIANCE ===
|
||||
`;
|
||||
return template.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成竞品合规约束(仅用于竞品对比图)
|
||||
* @param {object} sku - SKU数据
|
||||
* @returns {string} 约束文本
|
||||
*/
|
||||
function generateCompetitorConstraint(sku) {
|
||||
const productCategory = sku.product_name.includes('Cone') ? 'Plastic Cone' : 'Product';
|
||||
|
||||
const template = `
|
||||
=== COMPETITOR COMPARISON COMPLIANCE (LEGAL RED LINE) ===
|
||||
|
||||
This is a product comparison image. You MUST follow these legal requirements:
|
||||
|
||||
OUR PRODUCT SIDE (Left/Colored):
|
||||
- Show the Touchdog ${sku.product_name} in FULL COLOR
|
||||
- Product color: ${sku.color.hex} (${sku.color.name})
|
||||
- Use green checkmark icon (✓) to indicate positive
|
||||
- Label as "OUR" or "TOUCHDOG"
|
||||
- List ONLY the approved selling points from provided data
|
||||
- Product must match reference images exactly
|
||||
|
||||
COMPETITOR SIDE (Right/Grayscale):
|
||||
- Label as "OTHER" or "Traditional ${productCategory}" ONLY
|
||||
- DO NOT use any competitor brand names
|
||||
- DO NOT use any competitor logos or trademarks
|
||||
- DO NOT show any identifiable competitor product designs
|
||||
- Show a GENERIC, UNBRANDED version of traditional product
|
||||
- Apply GRAYSCALE filter to entire competitor side
|
||||
- Use red X icon to indicate negative
|
||||
|
||||
APPROVED COMPETITOR DISADVANTAGES (generic industry facts only):
|
||||
- "Heavy & Bulky"
|
||||
- "Blocks Vision & Movement"
|
||||
- "Hard to Store"
|
||||
- "Uncomfortable for Extended Wear"
|
||||
- "Difficult to Clean"
|
||||
- "Rigid and Inflexible"
|
||||
|
||||
VISUAL TREATMENT:
|
||||
- Our side: Vibrant, colorful, positive imagery
|
||||
- Competitor side: Muted, grayscale, neutral imagery
|
||||
- Clear visual contrast between the two sides
|
||||
- Center divider or clear separation between sides
|
||||
|
||||
STRICTLY PROHIBITED:
|
||||
- ❌ Any specific competitor brand name
|
||||
- ❌ Any competitor logo or trademark symbol
|
||||
- ❌ Any identifiable competitor product photo
|
||||
- ❌ Claims like "better than [Brand]"
|
||||
- ❌ Using competitor's actual product images
|
||||
|
||||
=== END COMPETITOR COMPARISON COMPLIANCE ===
|
||||
`;
|
||||
return template.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成品牌VI约束
|
||||
* @param {object} sku - SKU数据
|
||||
* @param {object} logoPlacement - Logo放置信息
|
||||
* @returns {string} 约束文本
|
||||
*/
|
||||
function generateBrandVIConstraint(sku, logoPlacement = {}) {
|
||||
const position = logoPlacement.position || 'bottom-right';
|
||||
const logoType = logoPlacement.type || 'combined';
|
||||
const logoColor = logoPlacement.color || 'red';
|
||||
|
||||
const template = `
|
||||
=== BRAND VI REQUIREMENTS ===
|
||||
|
||||
EMBROIDERED LOGO ON PRODUCT:
|
||||
- Text: "TOUCHDOG®" (UPPERCASE with ® symbol)
|
||||
- Position: Right petal segment, 2-3 o'clock position
|
||||
- Style: Embroidered, stitched into fabric
|
||||
- Color: Slightly darker shade of product color (${sku.color.hex})
|
||||
|
||||
E-COMMERCE IMAGE BRAND LOGO:
|
||||
- Position: ${position}
|
||||
- Type: ${logoType === 'combined' ? 'Graphic icon + "touchdog®" text + "wow pretty"' : logoType}
|
||||
- Color: ${logoColor === 'red' ? 'Red (#E60012)' : 'White'}
|
||||
|
||||
LOGO SPECIFICATIONS:
|
||||
- Clear space: Minimum 1/4 of logo height on all sides
|
||||
- Minimum size: Combined logo ≥ 46px height
|
||||
- Tagline: "wow pretty" (for international markets)
|
||||
|
||||
PROHIBITED LOGO MODIFICATIONS:
|
||||
- ❌ NO tilting or rotation
|
||||
- ❌ NO outlines or strokes
|
||||
- ❌ NO gradient fills
|
||||
- ❌ NO drop shadows
|
||||
- ❌ NO proportion changes
|
||||
- ❌ NO underlines
|
||||
|
||||
BRAND TYPOGRAPHY:
|
||||
- Headlines: Clean sans-serif, bold weight
|
||||
- Body text: Clean sans-serif, medium weight
|
||||
- Style: Clean, modern, professional
|
||||
|
||||
=== END BRAND VI REQUIREMENTS ===
|
||||
`;
|
||||
return template.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入所有约束到Prompt
|
||||
* @param {string} originalPrompt - 原始Prompt
|
||||
* @param {object} sku - SKU数据
|
||||
* @param {object} options - 选项
|
||||
* @param {boolean} options.isComparison - 是否是竞品对比图
|
||||
* @param {object} options.logoPlacement - Logo放置信息
|
||||
* @returns {string} 注入约束后的完整Prompt
|
||||
*/
|
||||
function injectConstraints(originalPrompt, sku, options = {}) {
|
||||
const { isComparison = false, logoPlacement = {} } = options;
|
||||
|
||||
const constraints = [];
|
||||
|
||||
// 始终添加产品一致性约束
|
||||
constraints.push(generateProductConsistencyConstraint(sku));
|
||||
|
||||
// 始终添加卖点合规约束
|
||||
constraints.push(generateSellingPointConstraint(sku));
|
||||
|
||||
// 如果是竞品对比图,添加竞品合规约束
|
||||
if (isComparison) {
|
||||
constraints.push(generateCompetitorConstraint(sku));
|
||||
}
|
||||
|
||||
// 始终添加品牌VI约束
|
||||
constraints.push(generateBrandVIConstraint(sku, logoPlacement));
|
||||
|
||||
// 添加参考图提示
|
||||
if (sku.ref_images && sku.ref_images.length > 0) {
|
||||
constraints.push(`
|
||||
=== REFERENCE IMAGES ===
|
||||
The following reference images must be used to ensure product consistency:
|
||||
${sku.ref_images.map((url, i) => `- Reference ${i + 1}: ${url}`).join('\n')}
|
||||
Product in generated image must match these reference images exactly.
|
||||
=== END REFERENCE IMAGES ===
|
||||
`);
|
||||
}
|
||||
|
||||
// 组合最终Prompt
|
||||
return `${originalPrompt}
|
||||
|
||||
${constraints.join('\n\n')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取图片的宽高比参数
|
||||
* @param {string} imageId - 图片ID (Main_01 或 APlus_01)
|
||||
* @returns {string} 宽高比参数
|
||||
*/
|
||||
function getAspectRatio(imageId) {
|
||||
if (imageId.startsWith('Main_')) {
|
||||
return '1:1';
|
||||
} else if (imageId.startsWith('APlus_')) {
|
||||
return '3:2';
|
||||
}
|
||||
return '1:1';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片尺寸
|
||||
* @param {string} imageId - 图片ID
|
||||
* @returns {object} 尺寸信息
|
||||
*/
|
||||
function getImageSize(imageId) {
|
||||
if (imageId.startsWith('Main_')) {
|
||||
return { width: 1600, height: 1600, label: '1600x1600' };
|
||||
} else if (imageId.startsWith('APlus_')) {
|
||||
return { width: 970, height: 600, label: '970x600' };
|
||||
}
|
||||
return { width: 1600, height: 1600, label: '1600x1600' };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
injectConstraints,
|
||||
generateProductConsistencyConstraint,
|
||||
generateSellingPointConstraint,
|
||||
generateCompetitorConstraint,
|
||||
generateBrandVIConstraint,
|
||||
getAspectRatio,
|
||||
getImageSize,
|
||||
renderTemplate,
|
||||
processEachBlocks
|
||||
};
|
||||
|
||||
|
||||
|
||||
338
lib/image-processor.js
Normal file
338
lib/image-processor.js
Normal file
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* 图片处理工具模块
|
||||
* 使用Sharp库实现:抠图、合成、叠加文字/标注
|
||||
*/
|
||||
|
||||
const sharp = require('sharp');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 调整图片大小,保持比例
|
||||
*/
|
||||
async function resizeImage(inputPath, width, height, fit = 'contain') {
|
||||
const buffer = await sharp(inputPath)
|
||||
.resize(width, height, { fit, background: { r: 255, g: 255, b: 255, alpha: 0 } })
|
||||
.toBuffer();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将图片转换为PNG(保持透明度)
|
||||
*/
|
||||
async function toPng(inputPath) {
|
||||
return await sharp(inputPath).png().toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建纯色背景
|
||||
*/
|
||||
async function createSolidBackground(width, height, color = '#FFFFFF') {
|
||||
// 解析颜色
|
||||
const r = parseInt(color.slice(1, 3), 16);
|
||||
const g = parseInt(color.slice(3, 5), 16);
|
||||
const b = parseInt(color.slice(5, 7), 16);
|
||||
|
||||
return await sharp({
|
||||
create: {
|
||||
width,
|
||||
height,
|
||||
channels: 3,
|
||||
background: { r, g, b }
|
||||
}
|
||||
}).jpeg().toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建渐变背景
|
||||
*/
|
||||
async function createGradientBackground(width, height, color1 = '#F5EDE4', color2 = '#FFFFFF') {
|
||||
// 创建简单的渐变效果(从上到下)
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:${color1};stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:${color2};stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#grad)"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return await sharp(Buffer.from(svg)).jpeg().toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 合成多个图层
|
||||
* @param {Buffer} baseImage - 底图
|
||||
* @param {Array} layers - 图层数组 [{buffer, left, top, width?, height?}]
|
||||
*/
|
||||
async function compositeImages(baseImage, layers) {
|
||||
let composite = sharp(baseImage);
|
||||
|
||||
const compositeInputs = [];
|
||||
|
||||
for (const layer of layers) {
|
||||
let input = layer.buffer;
|
||||
|
||||
// 如果需要调整大小
|
||||
if (layer.width || layer.height) {
|
||||
input = await sharp(layer.buffer)
|
||||
.resize(layer.width, layer.height, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
compositeInputs.push({
|
||||
input,
|
||||
left: layer.left || 0,
|
||||
top: layer.top || 0
|
||||
});
|
||||
}
|
||||
|
||||
return await composite.composite(compositeInputs).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在图片上叠加文字
|
||||
*/
|
||||
async function addTextOverlay(baseImage, texts) {
|
||||
// texts: [{text, x, y, fontSize, color, fontWeight, align}]
|
||||
|
||||
const metadata = await sharp(baseImage).metadata();
|
||||
const width = metadata.width;
|
||||
const height = metadata.height;
|
||||
|
||||
// 创建SVG文字层
|
||||
const textElements = texts.map(t => {
|
||||
const fontSize = t.fontSize || 40;
|
||||
const color = t.color || '#333333';
|
||||
const fontWeight = t.fontWeight || 'bold';
|
||||
const textAnchor = t.align === 'center' ? 'middle' : (t.align === 'right' ? 'end' : 'start');
|
||||
|
||||
return `<text x="${t.x}" y="${t.y}" font-size="${fontSize}" fill="${color}"
|
||||
font-weight="${fontWeight}" font-family="Arial, sans-serif" text-anchor="${textAnchor}">${escapeXml(t.text)}</text>`;
|
||||
}).join('\n');
|
||||
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}">
|
||||
${textElements}
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return await sharp(baseImage)
|
||||
.composite([{ input: Buffer.from(svg), top: 0, left: 0 }])
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建圆形裁剪的图片(用于细节放大镜效果)
|
||||
*/
|
||||
async function createCircularCrop(inputBuffer, diameter) {
|
||||
const circle = Buffer.from(`
|
||||
<svg width="${diameter}" height="${diameter}">
|
||||
<circle cx="${diameter/2}" cy="${diameter/2}" r="${diameter/2}" fill="white"/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
const resized = await sharp(inputBuffer)
|
||||
.resize(diameter, diameter, { fit: 'cover' })
|
||||
.toBuffer();
|
||||
|
||||
return await sharp(resized)
|
||||
.composite([{ input: circle, blend: 'dest-in' }])
|
||||
.png()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加圆形边框
|
||||
*/
|
||||
async function addCircleBorder(inputBuffer, diameter, borderWidth = 3, borderColor = '#2D4A3E') {
|
||||
const r = parseInt(borderColor.slice(1, 3), 16);
|
||||
const g = parseInt(borderColor.slice(3, 5), 16);
|
||||
const b = parseInt(borderColor.slice(5, 7), 16);
|
||||
|
||||
const borderSvg = Buffer.from(`
|
||||
<svg width="${diameter}" height="${diameter}">
|
||||
<circle cx="${diameter/2}" cy="${diameter/2}" r="${diameter/2 - borderWidth/2}"
|
||||
fill="none" stroke="rgb(${r},${g},${b})" stroke-width="${borderWidth}"/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
return await sharp(inputBuffer)
|
||||
.composite([{ input: borderSvg, top: 0, left: 0 }])
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带箭头的标注线
|
||||
*/
|
||||
async function createArrowLine(width, height, fromX, fromY, toX, toY, color = '#2D4A3E') {
|
||||
const r = parseInt(color.slice(1, 3), 16);
|
||||
const g = parseInt(color.slice(3, 5), 16);
|
||||
const b = parseInt(color.slice(5, 7), 16);
|
||||
|
||||
// 计算箭头角度
|
||||
const angle = Math.atan2(toY - fromY, toX - fromX);
|
||||
const arrowLength = 15;
|
||||
const arrowAngle = Math.PI / 6;
|
||||
|
||||
const arrow1X = toX - arrowLength * Math.cos(angle - arrowAngle);
|
||||
const arrow1Y = toY - arrowLength * Math.sin(angle - arrowAngle);
|
||||
const arrow2X = toX - arrowLength * Math.cos(angle + arrowAngle);
|
||||
const arrow2Y = toY - arrowLength * Math.sin(angle + arrowAngle);
|
||||
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}">
|
||||
<line x1="${fromX}" y1="${fromY}" x2="${toX}" y2="${toY}"
|
||||
stroke="rgb(${r},${g},${b})" stroke-width="2"/>
|
||||
<polygon points="${toX},${toY} ${arrow1X},${arrow1Y} ${arrow2X},${arrow2Y}"
|
||||
fill="rgb(${r},${g},${b})"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return Buffer.from(svg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建标题横幅
|
||||
*/
|
||||
async function createTitleBanner(width, height, text, bgColor = '#2D4A3E', textColor = '#FFFFFF') {
|
||||
const fontSize = Math.floor(height * 0.5);
|
||||
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}">
|
||||
<rect width="100%" height="100%" fill="${bgColor}" rx="5" ry="5"/>
|
||||
<text x="${width/2}" y="${height/2 + fontSize/3}" font-size="${fontSize}"
|
||||
fill="${textColor}" font-weight="bold" font-family="Arial, sans-serif"
|
||||
text-anchor="middle">"${escapeXml(text)}"</text>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return await sharp(Buffer.from(svg)).png().toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将图片转为灰度(用于竞品对比)
|
||||
*/
|
||||
async function toGrayscale(inputBuffer) {
|
||||
return await sharp(inputBuffer).grayscale().toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整图片亮度/对比度
|
||||
*/
|
||||
async function adjustBrightness(inputBuffer, brightness = 1.0) {
|
||||
return await sharp(inputBuffer)
|
||||
.modulate({ brightness })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加阴影效果
|
||||
*/
|
||||
async function addShadow(inputBuffer, offsetX = 5, offsetY = 5, blur = 10, opacity = 0.3) {
|
||||
const metadata = await sharp(inputBuffer).metadata();
|
||||
const width = metadata.width + offsetX + blur * 2;
|
||||
const height = metadata.height + offsetY + blur * 2;
|
||||
|
||||
// 创建阴影层
|
||||
const shadowBuffer = await sharp(inputBuffer)
|
||||
.greyscale()
|
||||
.modulate({ brightness: 0 })
|
||||
.blur(blur)
|
||||
.toBuffer();
|
||||
|
||||
// 创建透明背景
|
||||
const background = await sharp({
|
||||
create: {
|
||||
width,
|
||||
height,
|
||||
channels: 4,
|
||||
background: { r: 255, g: 255, b: 255, alpha: 0 }
|
||||
}
|
||||
}).png().toBuffer();
|
||||
|
||||
// 合成
|
||||
return await sharp(background)
|
||||
.composite([
|
||||
{ input: shadowBuffer, left: offsetX + blur, top: offsetY + blur, blend: 'over' },
|
||||
{ input: inputBuffer, left: blur, top: blur }
|
||||
])
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 裁剪图片的特定区域
|
||||
*/
|
||||
async function cropRegion(inputBuffer, left, top, width, height) {
|
||||
return await sharp(inputBuffer)
|
||||
.extract({ left, top, width, height })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:转义XML特殊字符
|
||||
*/
|
||||
function escapeXml(text) {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存图片到文件
|
||||
*/
|
||||
async function saveImage(buffer, outputPath, format = 'jpeg', quality = 90) {
|
||||
let pipeline = sharp(buffer);
|
||||
|
||||
if (format === 'jpeg' || format === 'jpg') {
|
||||
pipeline = pipeline.jpeg({ quality });
|
||||
} else if (format === 'png') {
|
||||
pipeline = pipeline.png();
|
||||
}
|
||||
|
||||
await pipeline.toFile(outputPath);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取图片为Buffer
|
||||
*/
|
||||
async function readImage(imagePath) {
|
||||
return fs.readFileSync(imagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片尺寸
|
||||
*/
|
||||
async function getImageSize(inputPath) {
|
||||
const metadata = await sharp(inputPath).metadata();
|
||||
return { width: metadata.width, height: metadata.height };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resizeImage,
|
||||
toPng,
|
||||
createSolidBackground,
|
||||
createGradientBackground,
|
||||
compositeImages,
|
||||
addTextOverlay,
|
||||
createCircularCrop,
|
||||
addCircleBorder,
|
||||
createArrowLine,
|
||||
createTitleBanner,
|
||||
toGrayscale,
|
||||
adjustBrightness,
|
||||
addShadow,
|
||||
cropRegion,
|
||||
saveImage,
|
||||
readImage,
|
||||
getImageSize
|
||||
};
|
||||
|
||||
|
||||
629
lib/template-config.js
Normal file
629
lib/template-config.js
Normal file
@@ -0,0 +1,629 @@
|
||||
/**
|
||||
* 可配置的套路模板系统
|
||||
* 运营人员可以自定义每张图的布局和内容
|
||||
*/
|
||||
|
||||
// ============================================================
|
||||
// 默认套路配置(基于真实交付成品分析)
|
||||
// ============================================================
|
||||
|
||||
const DEFAULT_MAIN_TEMPLATES = {
|
||||
// Main_01: 场景首图 + 卖点文字
|
||||
Main_01: {
|
||||
type: 'lifestyle_with_features',
|
||||
name: '场景首图+卖点',
|
||||
aspectRatio: '1:1',
|
||||
layout: {
|
||||
scene: 'left-center', // 场景图位置
|
||||
title: 'center-bottom', // 标题位置
|
||||
features: 'bottom-row' // 卖点位置
|
||||
},
|
||||
config: {
|
||||
title: 'DESIGNED FOR COMFORTABLE RECOVERY',
|
||||
titleStyle: 'curved-banner', // curved-banner | simple | none
|
||||
features: [
|
||||
{ icon: 'egg', text: 'LIGHTER THAN AN EGG' },
|
||||
{ icon: 'water', text: 'WATERPROOF & EASY WIPE' },
|
||||
{ icon: 'cloud', text: 'BREATHABLE COTTON LINING' }
|
||||
],
|
||||
background: 'warm-home'
|
||||
}
|
||||
},
|
||||
|
||||
// Main_02: 白底平铺 + 局部放大(关键改进!)
|
||||
Main_02: {
|
||||
type: 'product_with_callouts',
|
||||
name: '白底平铺+细节放大',
|
||||
aspectRatio: '1:1',
|
||||
layout: {
|
||||
product: 'center',
|
||||
callouts: 'corners' // 放大镜效果在角落
|
||||
},
|
||||
config: {
|
||||
title: 'DURABLE WATERPROOF PU LAYER',
|
||||
titlePosition: 'top',
|
||||
callouts: [
|
||||
{
|
||||
position: 'bottom-left',
|
||||
target: 'material-edge',
|
||||
label: 'DURABLE WATERPROOF PU LAYER',
|
||||
hasArrow: true
|
||||
},
|
||||
{
|
||||
position: 'bottom-right',
|
||||
target: 'neck-binding',
|
||||
label: 'DOUBLE-LAYER COMFORT',
|
||||
hasArrow: true
|
||||
}
|
||||
],
|
||||
background: 'white'
|
||||
}
|
||||
},
|
||||
|
||||
// Main_03: 功能调节展示
|
||||
Main_03: {
|
||||
type: 'feature_showcase',
|
||||
name: '功能调节展示',
|
||||
aspectRatio: '1:1',
|
||||
layout: {
|
||||
mainScene: 'left',
|
||||
featureCircles: 'right-column'
|
||||
},
|
||||
config: {
|
||||
title: 'ADJUSTABLE STRAP FOR A SECURE FIT',
|
||||
titlePosition: 'top-left',
|
||||
mainScene: {
|
||||
type: 'cat-wearing',
|
||||
background: 'lifestyle'
|
||||
},
|
||||
featureCircles: [
|
||||
{ label: 'SECURE THE ADJUSTABLE BELLY STRAP', showDetail: 'velcro-close-up' },
|
||||
{ label: 'ADJUST FOR A SNUG FIT', showDetail: 'full-product-view' }
|
||||
],
|
||||
background: 'matching-color' // 使用产品主色作为背景
|
||||
}
|
||||
},
|
||||
|
||||
// Main_04: 多场景使用(4宫格)
|
||||
Main_04: {
|
||||
type: 'multi_scenario_grid',
|
||||
name: '多场景使用',
|
||||
aspectRatio: '1:1',
|
||||
layout: {
|
||||
grid: '2x2',
|
||||
captionPosition: 'below-each'
|
||||
},
|
||||
config: {
|
||||
scenarios: [
|
||||
{ scene: 'standing', caption: '• HYGIENIC & EASY TO CLEAN', subtext: 'WATERPROOF OUTER LAYER' },
|
||||
{ scene: 'eating', caption: '• UNRESTRICTED EATING/DRINKING', subtext: 'SPECIALLY DESIGNED OPENING' },
|
||||
{ scene: 'playing', caption: '• REVERSIBLE WEAR', subtext: 'FLIP-OVER DESIGN' },
|
||||
{ scene: 'sleeping', caption: '• 360° COMFORT', subtext: 'FREE MOVEMENT' }
|
||||
],
|
||||
background: 'warm-beige'
|
||||
}
|
||||
},
|
||||
|
||||
// Main_05: 尺寸图
|
||||
Main_05: {
|
||||
type: 'size_chart',
|
||||
name: '尺寸图',
|
||||
aspectRatio: '1:1',
|
||||
layout: {
|
||||
product: 'top-center',
|
||||
table: 'bottom'
|
||||
},
|
||||
config: {
|
||||
title: 'PRODUCT SIZE',
|
||||
measurements: ['NECK', 'WIDTH'],
|
||||
sizeChart: {
|
||||
XS: { neck: '5.6-6.8IN', depth: '3.2IN' },
|
||||
S: { neck: '7.2-8.4IN', depth: '4IN' },
|
||||
M: { neck: '8.8-10.4IN', depth: '5IN' },
|
||||
L: { neck: '10.8-12.4IN', depth: '6IN' },
|
||||
XL: { neck: '12.8-14.4IN', depth: '7IN' }
|
||||
},
|
||||
footer: 'NOTE: ALWAYS MEASURE YOUR PET\'S NECK BEFORE SELECTING A SIZE'
|
||||
}
|
||||
},
|
||||
|
||||
// Main_06: 多角度展示
|
||||
Main_06: {
|
||||
type: 'multiple_angles',
|
||||
name: '多角度展示',
|
||||
aspectRatio: '1:1',
|
||||
layout: {
|
||||
images: 'side-by-side',
|
||||
divider: 'curved-line'
|
||||
},
|
||||
config: {
|
||||
angles: [
|
||||
{ view: 'front', petType: 'cat' },
|
||||
{ view: 'side', petType: 'cat' }
|
||||
],
|
||||
background: 'warm-interior'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_APLUS_TEMPLATES = {
|
||||
// APlus_01: 品牌横幅
|
||||
APlus_01: {
|
||||
type: 'brand_banner',
|
||||
name: '品牌横幅',
|
||||
aspectRatio: '3:2',
|
||||
config: {
|
||||
brandName: 'TOUCHDOG',
|
||||
productName: 'CAT SOFT CONE COLLAR',
|
||||
brandStyle: 'playful-curved', // 品牌字体风格
|
||||
brandColor: '#E8A87C', // 珊瑚色
|
||||
scene: 'cat-on-furniture',
|
||||
background: 'warm-home'
|
||||
}
|
||||
},
|
||||
|
||||
// APlus_02: 对比图(关键改进!)
|
||||
APlus_02: {
|
||||
type: 'comparison',
|
||||
name: '对比图',
|
||||
aspectRatio: '3:2',
|
||||
layout: {
|
||||
left: 'our-product',
|
||||
right: 'competitor',
|
||||
center: 'none' // 不要中间的产品图!
|
||||
},
|
||||
config: {
|
||||
leftSide: {
|
||||
label: 'OUR',
|
||||
labelBg: '#E8876C',
|
||||
checkmark: true,
|
||||
colorful: true,
|
||||
sellingPoints: [
|
||||
'CLOUD-LIGHT COMFORT',
|
||||
'WIDER & CLEARER',
|
||||
'FOLDABLE & PORTABLE'
|
||||
]
|
||||
},
|
||||
rightSide: {
|
||||
label: 'OTHER',
|
||||
labelBg: '#808080',
|
||||
xmark: true,
|
||||
grayscale: true,
|
||||
weaknesses: [
|
||||
'HEAVY & BULKY',
|
||||
'BLOCKS VISION & MOVEMENT',
|
||||
'HARD TO STORE'
|
||||
]
|
||||
},
|
||||
background: 'warm-beige'
|
||||
}
|
||||
},
|
||||
|
||||
// APlus_03: 功能细节
|
||||
APlus_03: {
|
||||
type: 'feature_details',
|
||||
name: '功能细节',
|
||||
aspectRatio: '3:2',
|
||||
config: {
|
||||
title: 'ENGINEERED FOR UNCOMPROMISED COMFORT',
|
||||
detailImages: [
|
||||
{ focus: 'inner-lining', caption: 'STURDY AND BREATHABLE', subtext: 'DURABLE AND COMFORTABLE' },
|
||||
{ focus: 'wearing-with-size', caption: 'EASY TO CLEAN, STYLISH', subtext: 'AND ATTRACTIVE' },
|
||||
{ focus: 'stitching-detail', caption: 'REINFORCED STITCHING', subtext: 'AND DURABLE FABRIC' }
|
||||
],
|
||||
background: 'warm-beige'
|
||||
}
|
||||
},
|
||||
|
||||
// APlus_04: 多场景横版
|
||||
APlus_04: {
|
||||
type: 'multi_scenario_horizontal',
|
||||
name: '多场景横版',
|
||||
aspectRatio: '3:2',
|
||||
config: {
|
||||
scenarios: [
|
||||
{ scene: 'standing', caption: '• HYGIENIC & EASY TO CLEAN' },
|
||||
{ scene: 'eating', caption: '• UNRESTRICTED EATING/DRINKING' },
|
||||
{ scene: 'playing', caption: '• REVERSIBLE WEAR' },
|
||||
{ scene: 'sleeping', caption: '• 360° COMFORT' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// APlus_05: 多角度横版
|
||||
APlus_05: {
|
||||
type: 'multiple_angles_horizontal',
|
||||
name: '多角度横版',
|
||||
aspectRatio: '3:2',
|
||||
config: {
|
||||
angles: ['front', 'side'],
|
||||
dividerStyle: 'curved'
|
||||
}
|
||||
},
|
||||
|
||||
// APlus_06: 尺寸表横版
|
||||
APlus_06: {
|
||||
type: 'size_chart_horizontal',
|
||||
name: '尺寸表横版',
|
||||
aspectRatio: '3:2',
|
||||
config: {
|
||||
title: 'PRODUCT SIZE',
|
||||
layout: 'product-left-table-right'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 从配置生成Prompt的函数
|
||||
// ============================================================
|
||||
|
||||
function generatePromptFromConfig(templateConfig, product, skuInfo) {
|
||||
const { type, config, layout } = templateConfig;
|
||||
|
||||
// 基础产品描述
|
||||
const productDesc = product.goldenDescription || 'Pet recovery cone collar';
|
||||
|
||||
switch (type) {
|
||||
case 'lifestyle_with_features':
|
||||
return generateLifestyleWithFeatures(productDesc, config, skuInfo);
|
||||
|
||||
case 'product_with_callouts':
|
||||
return generateProductWithCallouts(productDesc, config, skuInfo);
|
||||
|
||||
case 'feature_showcase':
|
||||
return generateFeatureShowcase(productDesc, config, skuInfo);
|
||||
|
||||
case 'multi_scenario_grid':
|
||||
return generateMultiScenarioGrid(productDesc, config, skuInfo);
|
||||
|
||||
case 'size_chart':
|
||||
return generateSizeChart(productDesc, config, skuInfo);
|
||||
|
||||
case 'multiple_angles':
|
||||
return generateMultipleAngles(productDesc, config, skuInfo);
|
||||
|
||||
case 'brand_banner':
|
||||
return generateBrandBanner(productDesc, config, skuInfo);
|
||||
|
||||
case 'comparison':
|
||||
return generateComparison(productDesc, config, skuInfo);
|
||||
|
||||
case 'feature_details':
|
||||
return generateFeatureDetails(productDesc, config, skuInfo);
|
||||
|
||||
default:
|
||||
return generateGenericPrompt(productDesc, config, skuInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 各类型Prompt生成函数
|
||||
// ============================================================
|
||||
|
||||
function generateLifestyleWithFeatures(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON MAIN IMAGE - LIFESTYLE WITH FEATURE TEXT]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
SCENE:
|
||||
- Beautiful ${skuInfo.petType || 'cat'} wearing the product comfortably
|
||||
- Warm, cozy home interior background (soft focus)
|
||||
- Product clearly visible, occupies 50-60% of frame
|
||||
- Pet looks comfortable and relaxed
|
||||
|
||||
TEXT OVERLAY REQUIREMENTS:
|
||||
- ${config.titleStyle === 'curved-banner' ? 'CURVED BANNER in muted blue (#8BB8C4) across center' : 'SIMPLE TITLE at center-bottom'}
|
||||
- TITLE TEXT: "${config.title}" in white, clean sans-serif font
|
||||
- BOTTOM ROW: 3 feature boxes in rounded rectangles
|
||||
${config.features.map((f, i) => ` - Box ${i+1}: "${f.text}" with ${f.icon} icon`).join('\n')}
|
||||
|
||||
STYLE:
|
||||
- Professional Amazon product photography
|
||||
- Warm color palette
|
||||
- Clean, readable text
|
||||
- Subtle paw print watermarks
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
|
||||
CRITICAL: Product must match reference image EXACTLY in shape, color, and material.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateProductWithCallouts(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON MAIN IMAGE - PRODUCT WITH DETAIL CALLOUTS]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT:
|
||||
- TOP: Title "${config.title}" in dark green banner with white text
|
||||
- CENTER: Product C-shape flat lay view on white/light background
|
||||
- CALLOUT CIRCLES: Magnifying glass style detail views with arrows pointing to product
|
||||
|
||||
CALLOUT DETAILS:
|
||||
${config.callouts.map(c => `- ${c.position.toUpperCase()}: Circular magnified view of ${c.target}
|
||||
Label: "${c.label}"
|
||||
Arrow pointing from circle to corresponding area on product`).join('\n')}
|
||||
|
||||
STYLE:
|
||||
- Clean product photography with infographic elements
|
||||
- Dark green (#2D4A3E) for title banner
|
||||
- Thin lines/arrows connecting callouts to product
|
||||
- Professional Amazon listing style
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
|
||||
CRITICAL: Product must show C-shaped opening clearly. Match reference image EXACTLY.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateFeatureShowcase(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON MAIN IMAGE - FEATURE SHOWCASE]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT:
|
||||
- BACKGROUND: Solid color matching product main color (use product's primary color)
|
||||
- TOP-LEFT: Large title "${config.title}" in dark text
|
||||
- LEFT SIDE (60%): Main lifestyle scene - ${skuInfo.petType || 'cat'} wearing product, walking through doorway/arch
|
||||
- RIGHT SIDE (40%): 2 feature detail circles stacked vertically
|
||||
|
||||
FEATURE CIRCLES:
|
||||
${config.featureCircles.map((f, i) => `${i+1}. Circle showing: ${f.showDetail}
|
||||
Button/label below: "${f.label}"`).join('\n')}
|
||||
|
||||
STYLE:
|
||||
- Lifestyle photography meets infographic
|
||||
- Rounded rectangle buttons for labels
|
||||
- Paw print decorations scattered on background
|
||||
- Modern, clean design
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
|
||||
CRITICAL: Product color and shape must match reference image EXACTLY.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateMultiScenarioGrid(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON MAIN IMAGE - MULTI-SCENARIO 2x2 GRID]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT: 2x2 grid of scenes, each in rounded rectangle frame
|
||||
|
||||
${config.scenarios.map((s, i) => `SCENE ${i+1} (${['TOP-LEFT', 'TOP-RIGHT', 'BOTTOM-LEFT', 'BOTTOM-RIGHT'][i]}):
|
||||
- ${skuInfo.petType || 'Cat'} ${s.scene} while wearing product
|
||||
- Caption below: "${s.caption}"
|
||||
- Subtext: "${s.subtext}"`).join('\n\n')}
|
||||
|
||||
BACKGROUND: Warm beige (#F5EDE4) with subtle paw print watermarks
|
||||
|
||||
STYLE:
|
||||
- Each scene in rounded rectangle with slight shadow
|
||||
- Captions in dark text, clean sans-serif
|
||||
- Professional lifestyle photography
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
|
||||
CRITICAL: Product in ALL 4 scenes must be identical and match reference image.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateSizeChart(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON MAIN IMAGE - SIZE CHART INFOGRAPHIC]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT:
|
||||
- TOP: Title "${config.title}" in bold dark text
|
||||
- CENTER: Product flat lay with measurement arrows and labels
|
||||
- Arrow across neck opening: "NECK"
|
||||
- Arrow across width: "WIDTH"
|
||||
- BOTTOM: Size chart table
|
||||
|
||||
SIZE CHART TABLE (clean, rounded corners):
|
||||
| SIZE | NECK CIRCUMFERENCE | DEPTH |
|
||||
${Object.entries(config.sizeChart).map(([size, dims]) =>
|
||||
`| ${size} | ${dims.neck} | ${dims.depth} |`).join('\n')}
|
||||
|
||||
FOOTER TEXT: "${config.footer}"
|
||||
|
||||
BACKGROUND: Warm beige with subtle paw prints
|
||||
|
||||
STYLE:
|
||||
- Clean infographic design
|
||||
- Table with alternating row colors
|
||||
- Professional product photography
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateMultipleAngles(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON MAIN IMAGE - MULTIPLE ANGLES]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT:
|
||||
- Split view with decorative curved divider in center
|
||||
- LEFT: ${skuInfo.petType || 'Cat'} wearing product, ${config.angles[0].view} view
|
||||
- RIGHT: Same ${skuInfo.petType || 'cat'} or similar, ${config.angles[1].view} view
|
||||
|
||||
SCENE:
|
||||
- Warm home interior background
|
||||
- Both pets look comfortable
|
||||
- Product clearly visible from different angles
|
||||
|
||||
STYLE:
|
||||
- Lifestyle photography
|
||||
- Soft warm lighting
|
||||
- Decorative curved line or wave between images
|
||||
- NO text overlay
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
|
||||
CRITICAL: Product must be IDENTICAL in both views, matching reference image exactly.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateBrandBanner(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON A+ BRAND BANNER - HORIZONTAL]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT (970x600px aspect ratio):
|
||||
- LEFT 40%: Brand text area with decorative elements
|
||||
- RIGHT 60%: Lifestyle scene
|
||||
|
||||
LEFT SIDE:
|
||||
- Brand name "${config.brandName}" in playful, slightly curved font
|
||||
- Color: ${config.brandColor} (coral/salmon)
|
||||
- Decorative paw prints around text
|
||||
- Product name "${config.productName}" below in smaller gray text
|
||||
|
||||
RIGHT SIDE:
|
||||
- ${skuInfo.petType || 'Cat'} wearing product on modern furniture
|
||||
- Warm cozy interior background
|
||||
|
||||
STYLE:
|
||||
- Professional Amazon A+ content
|
||||
- Warm, inviting color palette
|
||||
- 8K quality, ~1.6:1 aspect ratio
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateComparison(productDesc, config, skuInfo) {
|
||||
const { leftSide, rightSide } = config;
|
||||
|
||||
return `
|
||||
[AMAZON A+ COMPARISON IMAGE - SPLIT SCREEN]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT: Two-column comparison, NO center product image
|
||||
|
||||
LEFT SIDE (OUR PRODUCT):
|
||||
- GREEN CHECKMARK icon at top
|
||||
- "${leftSide.label}" label on ${leftSide.labelBg} background
|
||||
- ${skuInfo.petType || 'Cat'} wearing OUR PRODUCT (must match reference exactly!)
|
||||
- Cat looks HAPPY and comfortable
|
||||
- FULL COLOR, warm tones
|
||||
- Selling points in white text on colored background:
|
||||
${leftSide.sellingPoints.map(p => ` • ${p}`).join('\n')}
|
||||
|
||||
RIGHT SIDE (COMPETITOR):
|
||||
- RED X-MARK icon at top
|
||||
- "${rightSide.label}" label on ${rightSide.labelBg} background
|
||||
- ${skuInfo.petType || 'Cat'} wearing HARD PLASTIC transparent cone
|
||||
- Cat looks SAD/uncomfortable
|
||||
- GRAYSCALE/desaturated
|
||||
- Weaknesses in gray text:
|
||||
${rightSide.weaknesses.map(p => ` • ${p}`).join('\n')}
|
||||
|
||||
BACKGROUND: Warm beige (#F5EDE4) with paw print watermarks
|
||||
|
||||
STYLE:
|
||||
- Clear visual contrast between sides
|
||||
- Professional comparison layout
|
||||
- Clean readable text
|
||||
- 8K quality, ~1.6:1 aspect ratio
|
||||
|
||||
CRITICAL:
|
||||
- LEFT product must match reference image EXACTLY (our soft cone)
|
||||
- RIGHT shows generic hard plastic cone (NOT our product)
|
||||
- NO product image in the center
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateFeatureDetails(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON A+ FEATURE DETAILS - HORIZONTAL]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT:
|
||||
- TOP: Large title "${config.title}" in bold dark font
|
||||
- MIDDLE: 3 detail images in rounded rectangles, evenly spaced
|
||||
|
||||
${config.detailImages.map((d, i) => `DETAIL ${i+1}:
|
||||
- Focus: ${d.focus}
|
||||
- Caption: "${d.caption}"
|
||||
- Subtext: "${d.subtext}"`).join('\n\n')}
|
||||
|
||||
BACKGROUND: Warm beige (#F5EDE4) with subtle paw print watermarks
|
||||
|
||||
STYLE:
|
||||
- Professional product detail photography
|
||||
- Clean modern typography
|
||||
- Rounded rectangle frames for each detail
|
||||
- 8K quality, ~1.6:1 aspect ratio
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateGenericPrompt(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON PRODUCT IMAGE]
|
||||
|
||||
PRODUCT:
|
||||
${productDesc}
|
||||
|
||||
REQUIREMENTS:
|
||||
- Professional product photography
|
||||
- Match reference image exactly
|
||||
- High quality, clear details
|
||||
- 8K resolution
|
||||
`.trim();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 导出
|
||||
// ============================================================
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_MAIN_TEMPLATES,
|
||||
DEFAULT_APLUS_TEMPLATES,
|
||||
generatePromptFromConfig,
|
||||
|
||||
// 生成所有12张图的Prompts
|
||||
generateAllPrompts: (product, skuInfo, customConfig = {}) => {
|
||||
const mainTemplates = { ...DEFAULT_MAIN_TEMPLATES, ...customConfig.main };
|
||||
const aplusTemplates = { ...DEFAULT_APLUS_TEMPLATES, ...customConfig.aplus };
|
||||
|
||||
const prompts = [];
|
||||
|
||||
// 主图6张
|
||||
for (const [id, template] of Object.entries(mainTemplates)) {
|
||||
prompts.push({
|
||||
id,
|
||||
name: template.name,
|
||||
aspectRatio: template.aspectRatio || '1:1',
|
||||
type: template.type,
|
||||
prompt: generatePromptFromConfig(template, product, skuInfo)
|
||||
});
|
||||
}
|
||||
|
||||
// A+图6张
|
||||
for (const [id, template] of Object.entries(aplusTemplates)) {
|
||||
prompts.push({
|
||||
id,
|
||||
name: template.name,
|
||||
aspectRatio: template.aspectRatio || '3:2',
|
||||
type: template.type,
|
||||
prompt: generatePromptFromConfig(template, product, skuInfo)
|
||||
});
|
||||
}
|
||||
|
||||
return prompts;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
198
lib/vision-extractor.js
Normal file
198
lib/vision-extractor.js
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Vision产品特征提取器
|
||||
* 支持超时重试 + 缓存
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const API_KEY = 'G9rXx3Ag2Xfa7Gs8zou6t6HqeZ';
|
||||
const API_BASE = 'https://api.wuyinkeji.com/api';
|
||||
|
||||
// Vision提取Prompt
|
||||
const VISION_EXTRACT_PROMPT = `Analyze this pet product image in EXTREME detail.
|
||||
|
||||
Output ONLY a valid JSON object (no markdown, no explanation, no thinking):
|
||||
|
||||
{
|
||||
"color": {
|
||||
"primary": "<hex code like #C3E6E8>",
|
||||
"name": "<descriptive name like 'ice blue', 'mint green'>",
|
||||
"secondary": "<accent colors>"
|
||||
},
|
||||
"shape": {
|
||||
"type": "<flower/fan/cone/donut>",
|
||||
"petal_count": <number of segments>,
|
||||
"opening": "<C-shaped/full-circle/adjustable>",
|
||||
"description": "<detailed shape description>"
|
||||
},
|
||||
"material": {
|
||||
"type": "<PU/nylon/polyester/fabric>",
|
||||
"finish": "<glossy/matte/satin>",
|
||||
"texture": "<smooth/quilted/padded>"
|
||||
},
|
||||
"edge_binding": {
|
||||
"color": "<color of inner neck edge>",
|
||||
"material": "<ribbed elastic/fabric>"
|
||||
},
|
||||
"closure": {
|
||||
"type": "<velcro/button/snap>",
|
||||
"color": "<white/matching>",
|
||||
"position": "<location description>"
|
||||
},
|
||||
"logo": {
|
||||
"text": "<brand name>",
|
||||
"style": "<embroidered/printed/tag>",
|
||||
"position": "<location>"
|
||||
},
|
||||
"unique_features": ["<list distinctive features>"],
|
||||
"overall_description": "<2-3 sentence summary for image generation>"
|
||||
}`;
|
||||
|
||||
/**
|
||||
* 调用Vision API提取产品特征
|
||||
* @param {string} imageUrl - 图片URL
|
||||
* @param {object} options - 配置选项
|
||||
* @returns {object|null} - 提取的产品特征JSON
|
||||
*/
|
||||
async function extractProductFeatures(imageUrl, options = {}) {
|
||||
const {
|
||||
maxRetries = 3,
|
||||
timeout = 120000, // 120秒
|
||||
retryDelay = 5000,
|
||||
cacheDir = null,
|
||||
cacheKey = null
|
||||
} = options;
|
||||
|
||||
// 检查缓存
|
||||
if (cacheDir && cacheKey) {
|
||||
const cachePath = path.join(cacheDir, `vision-cache-${cacheKey}.json`);
|
||||
if (fs.existsSync(cachePath)) {
|
||||
try {
|
||||
const cached = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
||||
if (cached && cached.color) {
|
||||
console.log(' 📦 使用缓存的Vision结果');
|
||||
return cached;
|
||||
}
|
||||
} catch (e) {
|
||||
// 缓存无效,继续请求
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lastError = null;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
console.log(` 🔍 Vision分析 (尝试 ${attempt}/${maxRetries})...`);
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE}/chat/index`, {
|
||||
key: API_KEY,
|
||||
model: 'gemini-3-pro',
|
||||
content: VISION_EXTRACT_PROMPT,
|
||||
image_url: imageUrl
|
||||
}, { timeout });
|
||||
|
||||
const content = response.data.data?.choices?.[0]?.message?.content ||
|
||||
response.data.data?.content;
|
||||
|
||||
if (!content) {
|
||||
throw new Error('Vision响应为空');
|
||||
}
|
||||
|
||||
// 提取JSON(跳过thinking部分)
|
||||
let jsonStr = content;
|
||||
|
||||
// 如果有<think>标签,跳过它
|
||||
const thinkEnd = content.indexOf('</think>');
|
||||
if (thinkEnd !== -1) {
|
||||
jsonStr = content.substring(thinkEnd + 8);
|
||||
}
|
||||
|
||||
// 提取JSON
|
||||
const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
|
||||
// 验证必要字段
|
||||
if (parsed.color && parsed.shape) {
|
||||
console.log(' ✓ Vision提取成功');
|
||||
|
||||
// 保存缓存
|
||||
if (cacheDir && cacheKey) {
|
||||
const cachePath = path.join(cacheDir, `vision-cache-${cacheKey}.json`);
|
||||
fs.writeFileSync(cachePath, JSON.stringify(parsed, null, 2));
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('无法解析有效的JSON');
|
||||
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
console.log(` ⚠️ 尝试 ${attempt} 失败: ${error.message}`);
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
console.log(` ⏳ ${retryDelay/1000}秒后重试...`);
|
||||
await new Promise(r => setTimeout(r, retryDelay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.error(` ❌ Vision提取最终失败: ${lastError?.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Vision结果转换为Golden Description
|
||||
*/
|
||||
function buildGoldenDescription(visionResult, productType = 'pet recovery cone') {
|
||||
if (!visionResult) {
|
||||
return `
|
||||
PRODUCT: ${productType}
|
||||
- Shape: Soft flower/petal shape with C-shaped opening
|
||||
- Material: Soft waterproof fabric
|
||||
- Closure: Velcro strap
|
||||
- Comfortable design for pets
|
||||
|
||||
CRITICAL: Follow the reference image EXACTLY for product shape and color.
|
||||
`;
|
||||
}
|
||||
|
||||
const r = visionResult;
|
||||
|
||||
return `
|
||||
EXACT PRODUCT APPEARANCE (MUST MATCH REFERENCE IMAGE):
|
||||
|
||||
- Shape: ${r.shape?.petal_count || '7-8'}-PETAL ${(r.shape?.type || 'flower').toUpperCase()} shape
|
||||
- Opening: ${r.shape?.opening || 'C-shaped'} (NOT a full circle)
|
||||
- Color: ${(r.color?.name || 'ice blue').toUpperCase()} (${r.color?.primary || '#C3E6E8'})
|
||||
- Material: ${r.material?.finish || 'Soft'} ${r.material?.type || 'waterproof fabric'} with ${r.material?.texture || 'padded'} texture
|
||||
- Edge binding: ${r.edge_binding?.color || 'Matching color'} ${r.edge_binding?.material || 'ribbed elastic'} around inner neck hole
|
||||
- Closure: ${r.closure?.color || 'White'} ${r.closure?.type || 'velcro'} on ${r.closure?.position || 'one segment'}
|
||||
- Logo: "${r.logo?.text || 'TOUCHDOG'}" ${r.logo?.style || 'embroidered'} on ${r.logo?.position || 'one petal'}
|
||||
|
||||
UNIQUE FEATURES:
|
||||
${(r.unique_features || ['Scalloped petal edges', 'Radial stitching', 'Soft padded construction']).map(f => `- ${f}`).join('\n')}
|
||||
|
||||
OVERALL: ${r.overall_description || 'A soft, comfortable pet recovery cone with flower-petal design.'}
|
||||
|
||||
CRITICAL PROHIBITIONS:
|
||||
- ❌ NO printed colorful patterns (solid color only unless reference shows otherwise)
|
||||
- ❌ NO hard plastic transparent cones
|
||||
- ❌ NO fully circular/closed shapes (must match reference C-opening)
|
||||
- ❌ NO random brand logos
|
||||
- ❌ MUST match reference image product EXACTLY
|
||||
`.trim();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extractProductFeatures,
|
||||
buildGoldenDescription,
|
||||
VISION_EXTRACT_PROMPT
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user