Upload latest code and optimized prompts (v6)
This commit is contained in:
687
poc-workflow-v5.js
Normal file
687
poc-workflow-v5.js
Normal file
@@ -0,0 +1,687 @@
|
||||
/**
|
||||
* POC Workflow V5: P图合成工作流
|
||||
*
|
||||
* 核心策略:
|
||||
* - 产品部分:使用原始素材(100%一致)
|
||||
* - 背景/场景:AI生成(无产品)
|
||||
* - 文字排版:AI生成或代码叠加
|
||||
* - 最终图片:代码合成各层
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const sharp = require('sharp');
|
||||
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const imageProcessor = require('./lib/image-processor');
|
||||
|
||||
// ============================================================
|
||||
// 配置
|
||||
// ============================================================
|
||||
const API_KEY = 'G9rXx3Ag2Xfa7Gs8zou6t6HqeZ';
|
||||
const API_BASE = 'https://api.wuyinkeji.com/api';
|
||||
|
||||
// R2客户端
|
||||
const r2Client = new S3Client({
|
||||
region: 'auto',
|
||||
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
||||
},
|
||||
forcePathStyle: true,
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 工具函数
|
||||
// ============================================================
|
||||
|
||||
async function uploadToR2(buffer, filename) {
|
||||
const fileName = `v5-${Date.now()}-${filename}`;
|
||||
|
||||
await r2Client.send(new PutObjectCommand({
|
||||
Bucket: process.env.R2_BUCKET_NAME || 'ai-flow',
|
||||
Key: fileName,
|
||||
Body: buffer,
|
||||
ContentType: filename.endsWith('.png') ? 'image/png' : 'image/jpeg',
|
||||
}));
|
||||
|
||||
return process.env.R2_PUBLIC_DOMAIN
|
||||
? `${process.env.R2_PUBLIC_DOMAIN}/${fileName}`
|
||||
: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET_NAME}/${fileName}`;
|
||||
}
|
||||
|
||||
async function uploadFileToR2(filePath) {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
return await uploadToR2(buffer, path.basename(filePath));
|
||||
}
|
||||
|
||||
// 生图任务
|
||||
async function submitImageTask(prompt, refImageUrl = null, aspectRatio = '1:1') {
|
||||
const payload = {
|
||||
key: API_KEY,
|
||||
prompt: prompt,
|
||||
aspectRatio: aspectRatio,
|
||||
imageSize: '1K'
|
||||
};
|
||||
|
||||
if (refImageUrl) {
|
||||
payload.img_url = refImageUrl;
|
||||
}
|
||||
|
||||
const response = await axios.post(`${API_BASE}/img/nanoBanana-pro`, payload, { timeout: 30000 });
|
||||
const taskId = response.data.data?.id;
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('No task ID returned');
|
||||
}
|
||||
return taskId;
|
||||
}
|
||||
|
||||
async function pollImageResult(taskId) {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 90;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/img/drawDetail`, {
|
||||
params: { key: API_KEY, id: taskId },
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
const data = response.data.data;
|
||||
|
||||
if (data && data.status === 2 && data.image_url) {
|
||||
return data.image_url;
|
||||
} else if (data && data.status === 3) {
|
||||
throw new Error('Generation failed: ' + (data.fail_reason || 'Unknown'));
|
||||
}
|
||||
|
||||
process.stdout.write('.');
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
attempts++;
|
||||
} catch (error) {
|
||||
if (error.message.includes('Generation failed')) throw error;
|
||||
process.stdout.write('x');
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
throw new Error('Timeout');
|
||||
}
|
||||
|
||||
async function downloadImage(url) {
|
||||
const response = await axios.get(url, { responseType: 'arraybuffer', timeout: 30000 });
|
||||
return Buffer.from(response.data);
|
||||
}
|
||||
|
||||
async function generateAIImage(prompt, refImageUrl = null, aspectRatio = '1:1') {
|
||||
console.log(' 提交AI任务...');
|
||||
const taskId = await submitImageTask(prompt, refImageUrl, aspectRatio);
|
||||
console.log(` Task ID: ${taskId}`);
|
||||
|
||||
const imageUrl = await pollImageResult(taskId);
|
||||
console.log('\n ✓ AI生成完成');
|
||||
|
||||
return await downloadImage(imageUrl);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 12张图的生成逻辑
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Main_01: 场景首图+卖点
|
||||
* 策略:原图叠加到AI生成的背景上,再由AI添加文字
|
||||
*/
|
||||
async function generateMain01(materials, config, outputDir) {
|
||||
console.log('\n🎨 [Main_01] 场景首图+卖点');
|
||||
|
||||
// 直接用原佩戴图,让AI基于它编辑(添加文字和调整背景)
|
||||
const wornBuffer = fs.readFileSync(materials.wornImage);
|
||||
const wornUrl = await uploadToR2(wornBuffer, 'worn.jpg');
|
||||
|
||||
const prompt = `
|
||||
Edit this image of a cat wearing a pet recovery cone:
|
||||
1. KEEP the cat and product EXACTLY as shown - DO NOT modify them
|
||||
2. Enhance the background to be a warm, cozy home interior
|
||||
3. Add soft, warm lighting effects
|
||||
4. Add text overlay:
|
||||
- A curved blue banner across the middle-left with text "DESIGNED FOR COMFORTABLE RECOVERY"
|
||||
- 3 feature boxes at the bottom:
|
||||
* "LIGHTER THAN AN EGG" with egg icon
|
||||
* "WATERPROOF & EASY WIPE" with water droplet icon
|
||||
* "BREATHABLE COTTON LINING" with cloud icon
|
||||
5. Add subtle paw print decorations
|
||||
|
||||
Style: Professional Amazon product photography with text overlay
|
||||
Output: 1:1 square image, high quality
|
||||
`.trim();
|
||||
|
||||
const result = await generateAIImage(prompt, wornUrl, '1:1');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'Main_01.jpg'));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main_02: 白底平铺+细节放大
|
||||
* 策略:使用原平铺图,代码添加标注和放大镜效果
|
||||
*/
|
||||
async function generateMain02(materials, config, outputDir) {
|
||||
console.log('\n🎨 [Main_02] 白底平铺+细节放大');
|
||||
|
||||
// 读取原图
|
||||
const flatBuffer = fs.readFileSync(materials.flatImage);
|
||||
const metadata = await sharp(flatBuffer).metadata();
|
||||
|
||||
// 目标尺寸 1600x1600
|
||||
const targetSize = 1600;
|
||||
|
||||
// 调整原图大小,留出空间给标题和标注
|
||||
const productSize = Math.floor(targetSize * 0.65);
|
||||
const productBuffer = await sharp(flatBuffer)
|
||||
.resize(productSize, productSize, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 1 } })
|
||||
.toBuffer();
|
||||
|
||||
// 创建白色背景
|
||||
const background = await sharp({
|
||||
create: {
|
||||
width: targetSize,
|
||||
height: targetSize,
|
||||
channels: 3,
|
||||
background: { r: 255, g: 255, b: 255 }
|
||||
}
|
||||
}).jpeg().toBuffer();
|
||||
|
||||
// 创建标题横幅
|
||||
const bannerHeight = 80;
|
||||
const bannerSvg = `
|
||||
<svg width="${targetSize}" height="${bannerHeight}">
|
||||
<rect width="100%" height="100%" fill="#2D4A3E" rx="0" ry="0"/>
|
||||
<text x="${targetSize/2}" y="${bannerHeight/2 + 12}" font-size="32"
|
||||
fill="white" font-weight="bold" font-family="Arial, sans-serif"
|
||||
text-anchor="middle">"DURABLE WATERPROOF PU LAYER"</text>
|
||||
</svg>
|
||||
`;
|
||||
const bannerBuffer = await sharp(Buffer.from(bannerSvg)).png().toBuffer();
|
||||
|
||||
// 创建两个放大镜效果的细节图
|
||||
const detailSize = 180;
|
||||
|
||||
// 从原图裁剪细节区域(左下角材质,右下角内圈)
|
||||
const origWidth = metadata.width;
|
||||
const origHeight = metadata.height;
|
||||
|
||||
// 细节1:左下角区域
|
||||
const detail1 = await sharp(flatBuffer)
|
||||
.extract({
|
||||
left: Math.floor(origWidth * 0.1),
|
||||
top: Math.floor(origHeight * 0.6),
|
||||
width: Math.floor(origWidth * 0.25),
|
||||
height: Math.floor(origHeight * 0.25)
|
||||
})
|
||||
.resize(detailSize, detailSize, { fit: 'cover' })
|
||||
.toBuffer();
|
||||
|
||||
// 细节2:中心内圈区域
|
||||
const detail2 = await sharp(flatBuffer)
|
||||
.extract({
|
||||
left: Math.floor(origWidth * 0.35),
|
||||
top: Math.floor(origHeight * 0.35),
|
||||
width: Math.floor(origWidth * 0.3),
|
||||
height: Math.floor(origHeight * 0.3)
|
||||
})
|
||||
.resize(detailSize, detailSize, { fit: 'cover' })
|
||||
.toBuffer();
|
||||
|
||||
// 创建圆形遮罩
|
||||
const circleMask = Buffer.from(`
|
||||
<svg width="${detailSize}" height="${detailSize}">
|
||||
<circle cx="${detailSize/2}" cy="${detailSize/2}" r="${detailSize/2}" fill="white"/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
const detail1Circle = await sharp(detail1)
|
||||
.composite([{ input: circleMask, blend: 'dest-in' }])
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const detail2Circle = await sharp(detail2)
|
||||
.composite([{ input: circleMask, blend: 'dest-in' }])
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
// 添加绿色边框
|
||||
const borderSvg = Buffer.from(`
|
||||
<svg width="${detailSize}" height="${detailSize}">
|
||||
<circle cx="${detailSize/2}" cy="${detailSize/2}" r="${detailSize/2 - 2}"
|
||||
fill="none" stroke="#2D4A3E" stroke-width="4"/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
const detail1WithBorder = await sharp(detail1Circle)
|
||||
.composite([{ input: borderSvg }])
|
||||
.toBuffer();
|
||||
|
||||
const detail2WithBorder = await sharp(detail2Circle)
|
||||
.composite([{ input: borderSvg }])
|
||||
.toBuffer();
|
||||
|
||||
// 创建标签文字
|
||||
const label1Svg = Buffer.from(`
|
||||
<svg width="350" height="40">
|
||||
<text x="0" y="28" font-size="22" fill="#2D4A3E" font-weight="bold"
|
||||
font-family="Arial, sans-serif">DURABLE WATERPROOF PU LAYER</text>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
const label2Svg = Buffer.from(`
|
||||
<svg width="300" height="40">
|
||||
<text x="0" y="28" font-size="22" fill="#2D4A3E" font-weight="bold"
|
||||
font-family="Arial, sans-serif">DOUBLE-LAYER COMFORT</text>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
// 合成所有图层
|
||||
const productTop = bannerHeight + 50;
|
||||
const productLeft = (targetSize - productSize) / 2;
|
||||
|
||||
const composite = await sharp(background)
|
||||
.composite([
|
||||
{ input: bannerBuffer, top: 0, left: 0 },
|
||||
{ input: productBuffer, top: productTop, left: productLeft },
|
||||
{ input: detail1WithBorder, top: targetSize - detailSize - 120, left: 80 },
|
||||
{ input: await sharp(label1Svg).png().toBuffer(), top: targetSize - 80, left: 80 },
|
||||
{ input: detail2WithBorder, top: targetSize - detailSize - 120, left: targetSize - detailSize - 80 },
|
||||
{ input: await sharp(label2Svg).png().toBuffer(), top: targetSize - 80, left: targetSize - 300 - 80 }
|
||||
])
|
||||
.jpeg({ quality: 95 })
|
||||
.toBuffer();
|
||||
|
||||
await imageProcessor.saveImage(composite, path.join(outputDir, 'Main_02.jpg'));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main_03-06 和 APlus_01-06: 使用原图让AI编辑
|
||||
* 策略:传入原图,让AI保持产品不变,只做背景和文字编辑
|
||||
*/
|
||||
async function generateImageWithOriginal(id, name, materials, prompt, aspectRatio, outputDir) {
|
||||
console.log(`\n🎨 [${id}] ${name}`);
|
||||
|
||||
// 选择合适的参考图
|
||||
const refImage = id.includes('02') || id.includes('05') || id.includes('06')
|
||||
? materials.flatImage
|
||||
: materials.wornImage;
|
||||
|
||||
const refBuffer = fs.readFileSync(refImage);
|
||||
const refUrl = await uploadToR2(refBuffer, `ref-${id}.jpg`);
|
||||
|
||||
const result = await generateAIImage(prompt, refUrl, aspectRatio);
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, `${id}.jpg`));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* APlus_02: 对比图 - 特殊处理
|
||||
* 左边用原图,右边AI生成竞品
|
||||
*/
|
||||
async function generateAPlus02(materials, config, outputDir) {
|
||||
console.log('\n🎨 [APlus_02] 对比图');
|
||||
|
||||
// 用原佩戴图作为参考
|
||||
const wornBuffer = fs.readFileSync(materials.wornImage);
|
||||
const wornUrl = await uploadToR2(wornBuffer, 'comparison-ref.jpg');
|
||||
|
||||
const prompt = `
|
||||
Create an Amazon A+ comparison image (landscape 3:2 ratio):
|
||||
|
||||
LEFT SIDE (colorful, positive):
|
||||
- Show a happy cat/dog wearing the EXACT same soft cone collar from the reference image
|
||||
- The cone collar must match the reference EXACTLY in color, shape, and material
|
||||
- Green checkmark icon
|
||||
- Label "OUR" on coral/orange background
|
||||
- 3 selling points in white text:
|
||||
• CLOUD-LIGHT COMFORT
|
||||
• WIDER & CLEARER
|
||||
• FOLDABLE & PORTABLE
|
||||
|
||||
RIGHT SIDE (grayscale, negative):
|
||||
- Show a sad cat wearing a HARD PLASTIC transparent cone (traditional lampshade style)
|
||||
- Red X icon
|
||||
- Label "OTHER" on gray background
|
||||
- Grayscale/desaturated colors
|
||||
- 3 weaknesses in gray text:
|
||||
• HEAVY & BULKY
|
||||
• BLOCKS VISION & MOVEMENT
|
||||
• HARD TO STORE
|
||||
|
||||
CRITICAL:
|
||||
- LEFT product MUST match the reference image exactly (soft fabric cone)
|
||||
- RIGHT product should be a generic hard plastic cone (NOT our product)
|
||||
- NO product image in the center
|
||||
- Background: warm beige with paw print watermarks
|
||||
`.trim();
|
||||
|
||||
const result = await generateAIImage(prompt, wornUrl, '3:2');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'APlus_02.jpg'));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 主流程
|
||||
// ============================================================
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
let materialDir = path.join(__dirname, '素材/素材/已有的素材');
|
||||
let skuName = 'Touchdog冰蓝色伊丽莎白圈';
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith('--material-dir=')) {
|
||||
materialDir = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--sku-name=')) {
|
||||
skuName = arg.split('=')[1];
|
||||
}
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
|
||||
const safeName = skuName.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_').slice(0, 20);
|
||||
const outputDir = path.join(__dirname, `output_v5_${safeName}_${timestamp}`);
|
||||
|
||||
console.log('\n' + '═'.repeat(70));
|
||||
console.log('🚀 POC Workflow V5 - P图合成工作流');
|
||||
console.log('═'.repeat(70));
|
||||
console.log('\n📋 核心策略: 保持原素材产品不变,AI只编辑背景和文字');
|
||||
console.log(' 素材目录:', materialDir);
|
||||
console.log(' 输出目录:', outputDir);
|
||||
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 扫描素材
|
||||
const files = fs.readdirSync(materialDir);
|
||||
const images = files.filter(f => /\.(jpg|jpeg|png)$/i.test(f));
|
||||
|
||||
// 识别平铺图和佩戴图
|
||||
let flatImage = images.find(i => i.includes('5683') || i.includes('1.png'));
|
||||
let wornImage = images.find(i => i.includes('6514') || i.includes('3.png') || i.includes('3.jpg'));
|
||||
|
||||
if (!flatImage) flatImage = images.find(i => i.endsWith('.png')) || images[0];
|
||||
if (!wornImage) wornImage = images.find(i => i.endsWith('.jpg') || i.endsWith('.JPG')) || flatImage;
|
||||
|
||||
const materials = {
|
||||
flatImage: path.join(materialDir, flatImage),
|
||||
wornImage: path.join(materialDir, wornImage)
|
||||
};
|
||||
|
||||
console.log('\n📁 素材:');
|
||||
console.log(' 平铺图:', flatImage);
|
||||
console.log(' 佩戴图:', wornImage);
|
||||
|
||||
const config = { brandName: 'TOUCHDOG', productName: 'CAT SOFT CONE COLLAR' };
|
||||
const results = [];
|
||||
|
||||
// ========== 生成12张图 ==========
|
||||
console.log('\n' + '─'.repeat(70));
|
||||
console.log('🎨 开始生成12张图');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
try {
|
||||
// Main_01: 场景首图
|
||||
results.push({ id: 'Main_01', success: await generateMain01(materials, config, outputDir) });
|
||||
} catch (e) {
|
||||
console.error('Main_01 失败:', e.message);
|
||||
results.push({ id: 'Main_01', success: false, error: e.message });
|
||||
}
|
||||
|
||||
try {
|
||||
// Main_02: 白底平铺+细节(代码合成)
|
||||
results.push({ id: 'Main_02', success: await generateMain02(materials, config, outputDir) });
|
||||
} catch (e) {
|
||||
console.error('Main_02 失败:', e.message);
|
||||
results.push({ id: 'Main_02', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// Main_03: 功能调节展示
|
||||
try {
|
||||
const prompt03 = `
|
||||
Edit this image to create an Amazon product feature showcase:
|
||||
1. KEEP the pet and product EXACTLY as shown
|
||||
2. Use a background color that matches the product's main color
|
||||
3. Add title at top-left: "ADJUSTABLE STRAP FOR A SECURE FIT"
|
||||
4. Add 2 circular detail callouts on the right side:
|
||||
- One showing velcro closure detail, label: "SECURE THE ADJUSTABLE STRAP"
|
||||
- One showing the product from another angle, label: "ADJUST FOR A SNUG FIT"
|
||||
5. Add paw print decorations
|
||||
Style: Professional Amazon infographic, 1:1 square
|
||||
`.trim();
|
||||
results.push({ id: 'Main_03', success: await generateImageWithOriginal('Main_03', '功能调节展示', materials, prompt03, '1:1', outputDir) });
|
||||
} catch (e) {
|
||||
console.error('Main_03 失败:', e.message);
|
||||
results.push({ id: 'Main_03', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// Main_04: 多场景使用
|
||||
try {
|
||||
const prompt04 = `
|
||||
Create a 2x2 grid Amazon product image showing 4 usage scenarios:
|
||||
Based on the reference image, show the SAME product (keep it identical) in 4 scenes:
|
||||
|
||||
TOP-LEFT: Cat standing, wearing the product
|
||||
Caption: "• HYGIENIC & EASY TO CLEAN"
|
||||
|
||||
TOP-RIGHT: Cat eating from bowl while wearing product
|
||||
Caption: "• UNRESTRICTED EATING/DRINKING"
|
||||
|
||||
BOTTOM-LEFT: Cat playing/stretching with product
|
||||
Caption: "• REVERSIBLE WEAR"
|
||||
|
||||
BOTTOM-RIGHT: Cat sleeping peacefully with product
|
||||
Caption: "• 360° COMFORT"
|
||||
|
||||
CRITICAL: The product in ALL 4 scenes must match the reference image EXACTLY
|
||||
Background: Warm beige with paw print watermarks
|
||||
Style: Rounded rectangle frames for each scene
|
||||
`.trim();
|
||||
results.push({ id: 'Main_04', success: await generateImageWithOriginal('Main_04', '多场景使用', materials, prompt04, '1:1', outputDir) });
|
||||
} catch (e) {
|
||||
console.error('Main_04 失败:', e.message);
|
||||
results.push({ id: 'Main_04', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// Main_05: 尺寸图(代码生成)
|
||||
try {
|
||||
console.log('\n🎨 [Main_05] 尺寸图');
|
||||
|
||||
const flatBuffer = fs.readFileSync(materials.flatImage);
|
||||
const targetSize = 1600;
|
||||
const productSize = Math.floor(targetSize * 0.5);
|
||||
|
||||
const productBuffer = await sharp(flatBuffer)
|
||||
.resize(productSize, productSize, { fit: 'contain', background: { r: 245, g: 237, b: 228, alpha: 1 } })
|
||||
.toBuffer();
|
||||
|
||||
// 创建米色背景
|
||||
const background = await sharp({
|
||||
create: { width: targetSize, height: targetSize, channels: 3, background: { r: 245, g: 237, b: 228 } }
|
||||
}).jpeg().toBuffer();
|
||||
|
||||
// 标题
|
||||
const titleSvg = Buffer.from(`
|
||||
<svg width="${targetSize}" height="100">
|
||||
<text x="${targetSize/2}" y="70" font-size="48" fill="#333" font-weight="bold"
|
||||
font-family="Arial, sans-serif" text-anchor="middle">PRODUCT SIZE</text>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
// 尺寸表
|
||||
const tableSvg = Buffer.from(`
|
||||
<svg width="800" height="400">
|
||||
<rect x="0" y="0" width="800" height="50" fill="#E8D5C4" rx="5"/>
|
||||
<text x="100" y="35" font-size="20" fill="#333" font-weight="bold" font-family="Arial">SIZE</text>
|
||||
<text x="350" y="35" font-size="20" fill="#333" font-weight="bold" font-family="Arial">NECK</text>
|
||||
<text x="600" y="35" font-size="20" fill="#333" font-weight="bold" font-family="Arial">DEPTH</text>
|
||||
|
||||
<rect x="0" y="55" width="800" height="45" fill="#FFF"/>
|
||||
<text x="100" y="85" font-size="18" fill="#333" font-family="Arial">XS</text>
|
||||
<text x="350" y="85" font-size="18" fill="#333" font-family="Arial">5.6-6.8IN</text>
|
||||
<text x="600" y="85" font-size="18" fill="#333" font-family="Arial">3.2IN</text>
|
||||
|
||||
<rect x="0" y="105" width="800" height="45" fill="#F5F5F5"/>
|
||||
<text x="100" y="135" font-size="18" fill="#333" font-family="Arial">S</text>
|
||||
<text x="350" y="135" font-size="18" fill="#333" font-family="Arial">7.2-8.4IN</text>
|
||||
<text x="600" y="135" font-size="18" fill="#333" font-family="Arial">4IN</text>
|
||||
|
||||
<rect x="0" y="155" width="800" height="45" fill="#FFF"/>
|
||||
<text x="100" y="185" font-size="18" fill="#333" font-family="Arial">M</text>
|
||||
<text x="350" y="185" font-size="18" fill="#333" font-family="Arial">8.8-10.4IN</text>
|
||||
<text x="600" y="185" font-size="18" fill="#333" font-family="Arial">5IN</text>
|
||||
|
||||
<rect x="0" y="205" width="800" height="45" fill="#F5F5F5"/>
|
||||
<text x="100" y="235" font-size="18" fill="#333" font-family="Arial">L</text>
|
||||
<text x="350" y="235" font-size="18" fill="#333" font-family="Arial">10.8-12.4IN</text>
|
||||
<text x="600" y="235" font-size="18" fill="#333" font-family="Arial">6IN</text>
|
||||
|
||||
<rect x="0" y="255" width="800" height="45" fill="#FFF"/>
|
||||
<text x="100" y="285" font-size="18" fill="#333" font-family="Arial">XL</text>
|
||||
<text x="350" y="285" font-size="18" fill="#333" font-family="Arial">12.8-14.4IN</text>
|
||||
<text x="600" y="285" font-size="18" fill="#333" font-family="Arial">7IN</text>
|
||||
|
||||
<text x="400" y="340" font-size="14" fill="#E8876C" font-family="Arial" text-anchor="middle">
|
||||
NOTE: ALWAYS MEASURE YOUR PET'S NECK BEFORE SELECTING A SIZE
|
||||
</text>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
const composite = await sharp(background)
|
||||
.composite([
|
||||
{ input: await sharp(titleSvg).png().toBuffer(), top: 20, left: 0 },
|
||||
{ input: productBuffer, top: 120, left: (targetSize - productSize) / 2 },
|
||||
{ input: await sharp(tableSvg).png().toBuffer(), top: targetSize - 450, left: (targetSize - 800) / 2 }
|
||||
])
|
||||
.jpeg({ quality: 95 })
|
||||
.toBuffer();
|
||||
|
||||
await imageProcessor.saveImage(composite, path.join(outputDir, 'Main_05.jpg'));
|
||||
results.push({ id: 'Main_05', success: true });
|
||||
} catch (e) {
|
||||
console.error('Main_05 失败:', e.message);
|
||||
results.push({ id: 'Main_05', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// Main_06: 多角度展示
|
||||
try {
|
||||
const prompt06 = `
|
||||
Edit this image to show multiple angles:
|
||||
1. Split the image into two parts with a curved decorative divider
|
||||
2. LEFT: Keep the cat wearing product as shown
|
||||
3. RIGHT: Show the same cat from a different angle (side view)
|
||||
4. CRITICAL: The product must be IDENTICAL in both views
|
||||
5. Warm home interior background
|
||||
6. NO text overlay
|
||||
Style: Professional lifestyle photography, 1:1 square
|
||||
`.trim();
|
||||
results.push({ id: 'Main_06', success: await generateImageWithOriginal('Main_06', '多角度展示', materials, prompt06, '1:1', outputDir) });
|
||||
} catch (e) {
|
||||
console.error('Main_06 失败:', e.message);
|
||||
results.push({ id: 'Main_06', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// APlus_01: 品牌横幅
|
||||
try {
|
||||
const prompt_a01 = `
|
||||
Create an Amazon A+ brand banner (landscape 3:2):
|
||||
1. LEFT 40%: Brand text area
|
||||
- "TOUCHDOG" in playful coral/salmon color (#E8A87C) with paw prints
|
||||
- "CAT SOFT CONE COLLAR" below in gray
|
||||
2. RIGHT 60%: Show the cat wearing the product EXACTLY as in reference
|
||||
3. Warm cozy home background
|
||||
4. The product must match the reference EXACTLY
|
||||
`.trim();
|
||||
results.push({ id: 'APlus_01', success: await generateImageWithOriginal('APlus_01', '品牌横幅', materials, prompt_a01, '3:2', outputDir) });
|
||||
} catch (e) {
|
||||
console.error('APlus_01 失败:', e.message);
|
||||
results.push({ id: 'APlus_01', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// APlus_02: 对比图
|
||||
try {
|
||||
results.push({ id: 'APlus_02', success: await generateAPlus02(materials, config, outputDir) });
|
||||
} catch (e) {
|
||||
console.error('APlus_02 失败:', e.message);
|
||||
results.push({ id: 'APlus_02', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// APlus_03-06: 其他A+图
|
||||
const aplusPrompts = {
|
||||
'APlus_03': {
|
||||
name: '功能细节',
|
||||
prompt: `Create Amazon A+ feature details (3:2):
|
||||
Title: "ENGINEERED FOR UNCOMPROMISED COMFORT"
|
||||
Show 3 detail images with captions:
|
||||
1. Inner lining close-up: "STURDY AND BREATHABLE"
|
||||
2. Pet wearing with size marker: "EASY TO CLEAN, STYLISH"
|
||||
3. Stitching detail: "REINFORCED STITCHING"
|
||||
Product must match reference EXACTLY. Warm beige background.`
|
||||
},
|
||||
'APlus_04': {
|
||||
name: '多场景横版',
|
||||
prompt: `Create Amazon A+ horizontal 4-scene image (3:2):
|
||||
4 scenes in a row, each showing the SAME product (match reference):
|
||||
1. Cat standing: "HYGIENIC & EASY TO CLEAN"
|
||||
2. Cat eating: "UNRESTRICTED EATING"
|
||||
3. Cat playing: "REVERSIBLE WEAR"
|
||||
4. Cat sleeping: "360° COMFORT"
|
||||
Warm beige background with paw prints.`
|
||||
},
|
||||
'APlus_05': {
|
||||
name: '多角度横版',
|
||||
prompt: `Create Amazon A+ multiple angles image (3:2):
|
||||
Show 2 views side by side with curved divider:
|
||||
LEFT: Front view of cat wearing product
|
||||
RIGHT: Side view of same product
|
||||
Product must match reference EXACTLY. NO text. Warm background.`
|
||||
},
|
||||
'APlus_06': {
|
||||
name: '尺寸表横版',
|
||||
prompt: `Create Amazon A+ size chart (3:2):
|
||||
LEFT: Product flat lay with dimension arrows (NECK, WIDTH)
|
||||
RIGHT: Size chart table (XS to XL)
|
||||
Title: "PRODUCT SIZE"
|
||||
Product must match reference EXACTLY. Warm beige background.`
|
||||
}
|
||||
};
|
||||
|
||||
for (const [id, info] of Object.entries(aplusPrompts)) {
|
||||
try {
|
||||
results.push({ id, success: await generateImageWithOriginal(id, info.name, materials, info.prompt, '3:2', outputDir) });
|
||||
} catch (e) {
|
||||
console.error(`${id} 失败:`, e.message);
|
||||
results.push({ id, success: false, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 总结 ==========
|
||||
console.log('\n' + '═'.repeat(70));
|
||||
console.log('📊 生成完成');
|
||||
console.log('═'.repeat(70));
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
console.log(`\n✅ 成功: ${successCount}/${results.length}`);
|
||||
|
||||
if (successCount < results.length) {
|
||||
console.log('\n❌ 失败:');
|
||||
results.filter(r => !r.success).forEach(r => console.log(` - ${r.id}: ${r.error}`));
|
||||
}
|
||||
|
||||
console.log(`\n📁 输出目录: ${outputDir}`);
|
||||
|
||||
fs.writeFileSync(path.join(outputDir, 'results.json'), JSON.stringify(results, null, 2));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user