/**
* 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 = `
`;
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(`
`);
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(`
`);
const detail1WithBorder = await sharp(detail1Circle)
.composite([{ input: borderSvg }])
.toBuffer();
const detail2WithBorder = await sharp(detail2Circle)
.composite([{ input: borderSvg }])
.toBuffer();
// 创建标签文字
const label1Svg = Buffer.from(`
`);
const label2Svg = Buffer.from(`
`);
// 合成所有图层
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(`
`);
// 尺寸表
const tableSvg = Buffer.from(`
`);
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);